mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-31 13:29:55 +00:00
add support for dynamic combos on subgraphs
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
demoteWidget,
|
||||
isDynamicComboChild,
|
||||
promoteRecommendedWidgets
|
||||
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
@@ -37,6 +38,10 @@ type Overlay = Partial<IBaseWidget> & {
|
||||
widgetName: string
|
||||
isProxyWidget: boolean
|
||||
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.
|
||||
// the _overlay property can be used to directly access the Overlay object
|
||||
@@ -169,7 +174,7 @@ function resolveLinkedWidget(
|
||||
const n = getNodeByExecutionId(graph, nodeId)
|
||||
if (!n) return [undefined, undefined]
|
||||
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))
|
||||
widget.computedHeight = 20
|
||||
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
|
||||
* Many arguments are shared between function calls
|
||||
@@ -201,6 +218,13 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
*/
|
||||
const handler = {
|
||||
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 redirectedReceiver = receiver
|
||||
if (property == '_overlay') return overlay
|
||||
@@ -220,9 +244,10 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
if (linkedNode && linkedWidget?.computedDisabled) {
|
||||
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)
|
||||
backingWidget = linkedWidget ?? disconnectedWidget
|
||||
updateHiddenState()
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(overlay, property)) {
|
||||
redirectedTarget = overlay
|
||||
|
||||
@@ -16,31 +16,106 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
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]
|
||||
|
||||
function getProxyWidgets(node: SubgraphNode) {
|
||||
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(
|
||||
node: PartialNode,
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
for (const parent of 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
|
||||
promoteWidgetsToProxy(node, getWidgetWithChildren(node, widget), parents)
|
||||
}
|
||||
|
||||
export function demoteWidget(
|
||||
@@ -48,13 +123,17 @@ export function demoteWidget(
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
const widgetsToDemote = getWidgetWithChildren(node, widget)
|
||||
for (const parent of parents) {
|
||||
const proxyWidgets = getProxyWidgets(parent).filter(
|
||||
(widgetItem) => !matchesPropertyItem([node, widget])(widgetItem)
|
||||
(widgetItem) =>
|
||||
!widgetsToDemote.some((w) => matchesPropertyItem([node, w])(widgetItem))
|
||||
)
|
||||
parent.properties.proxyWidgets = proxyWidgets
|
||||
}
|
||||
widget.promoted = false
|
||||
for (const w of widgetsToDemote) {
|
||||
w.promoted = false
|
||||
}
|
||||
}
|
||||
|
||||
export function matchesWidgetItem([nodeId, widgetName]: [string, string]) {
|
||||
@@ -68,7 +147,56 @@ export function widgetItemToProperty([n, w]: WidgetItem): [string, string] {
|
||||
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
|
||||
//This function will require rework to properly support linked subgraphs
|
||||
//Either by including actual parents in the navigation stack,
|
||||
@@ -108,6 +236,8 @@ export function addWidgetPromotionOptions(
|
||||
}
|
||||
})
|
||||
else {
|
||||
if (isChildOfPromotedDynamicCombo(node, widget)) return
|
||||
|
||||
options.unshift({
|
||||
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
@@ -127,9 +257,12 @@ export function tryToggleWidgetPromotion() {
|
||||
const promotableParents = parents.filter(
|
||||
(s) => !getProxyWidgets(s).some(matchesPropertyItem([node, widget]))
|
||||
)
|
||||
if (promotableParents.length > 0)
|
||||
if (promotableParents.length > 0) {
|
||||
promoteWidget(node, widget, promotableParents)
|
||||
else demoteWidget(node, widget, parents)
|
||||
} else {
|
||||
if (isChildOfPromotedDynamicCombo(node, widget)) return
|
||||
demoteWidget(node, widget, parents)
|
||||
}
|
||||
}
|
||||
const recommendedNodes = [
|
||||
'CLIPTextEncode',
|
||||
|
||||
@@ -2,6 +2,10 @@ import { remove } from 'es-toolkit'
|
||||
import { shallowReactive } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import {
|
||||
autoPromoteDynamicChildren,
|
||||
invalidateProxyWidgetsForNode
|
||||
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import type {
|
||||
ISlotType,
|
||||
INodeInputSlot,
|
||||
@@ -126,7 +130,13 @@ function dynamicComboWidget(
|
||||
ensureWidgetForInput(node, newInput)
|
||||
}
|
||||
}
|
||||
|
||||
const childWidgets = node.widgets!.filter(isInGroup)
|
||||
for (const child of childWidgets) {
|
||||
child.dynamicWidgetParent = widget.name
|
||||
}
|
||||
})
|
||||
widget.dynamicWidgetRoot = true
|
||||
|
||||
const inputInsertionPoint =
|
||||
node.inputs.findIndex((i) => i.name === widget.name) + 1
|
||||
@@ -179,6 +189,12 @@ function dynamicComboWidget(
|
||||
if (!node.graph) return
|
||||
node._setConcreteSlots()
|
||||
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)
|
||||
}
|
||||
//A little hacky, but onConfigure won't work.
|
||||
|
||||
@@ -361,6 +361,16 @@ export interface IBaseWidget<
|
||||
|
||||
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
|
||||
callback?(
|
||||
value: unknown,
|
||||
|
||||
Reference in New Issue
Block a user