mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
refactor: replace widget copy with lightweight stub in SubgraphNode._setWidget
Replace the heavy createCopyForNode/Object.defineProperties pattern with a minimal stub that only carries metadata (sourceNodeId, sourceWidgetName) and delegates value/type/options to the interior widget. - Remove BaseWidget and AssetWidget imports from SubgraphNode - Trigger syncPromotedWidgets for live connections via proxyWidgets setter - Patch input._widget references after stub-to-PromotedWidgetSlot replacement - Resolve legacy -1 entries via subgraph input wiring instead of copy metadata - Add optional slotName parameter to PromotedWidgetSlot constructor Amp-Thread-ID: https://ampcode.com/threads/T-019c5551-e9c9-754a-afdd-94537f2542b3 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -45,9 +45,10 @@ export class PromotedWidgetSlot extends BaseWidget<IBaseWidget> {
|
||||
constructor(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: NodeId,
|
||||
sourceWidgetName: string
|
||||
sourceWidgetName: string,
|
||||
slotName?: string
|
||||
) {
|
||||
const name = `${sourceNodeId}: ${sourceWidgetName}`
|
||||
const name = slotName ?? `${sourceNodeId}: ${sourceWidgetName}`
|
||||
super(
|
||||
{
|
||||
name,
|
||||
|
||||
@@ -34,6 +34,7 @@ function createMockSubgraphNode(
|
||||
return {
|
||||
isSubgraphNode: () => true,
|
||||
widgets,
|
||||
inputs: [],
|
||||
properties: { proxyWidgets: [] },
|
||||
_setConcreteSlots: vi.fn(),
|
||||
arrange: vi.fn()
|
||||
|
||||
@@ -63,9 +63,8 @@ function syncPromotedWidgets(
|
||||
if (w instanceof PromotedWidgetSlot) w.disposeDomAdapter()
|
||||
}
|
||||
|
||||
// Collect slot-promoted copies created by _setWidget() during configure.
|
||||
// These have sourceNodeId/sourceWidgetName set via Object.defineProperties.
|
||||
const copies = widgets.filter(
|
||||
// Collect stubs created by _setWidget() during configure.
|
||||
const stubs = widgets.filter(
|
||||
(
|
||||
w
|
||||
): w is IBaseWidget & { sourceNodeId: string; sourceWidgetName: string } =>
|
||||
@@ -74,55 +73,84 @@ function syncPromotedWidgets(
|
||||
'sourceWidgetName' in w
|
||||
)
|
||||
|
||||
// Remove all promoted widgets (both PromotedWidgetSlots and copies)
|
||||
// Remove all promoted widgets (both PromotedWidgetSlots and stubs)
|
||||
node.widgets = widgets.filter(
|
||||
(w) =>
|
||||
!(w instanceof PromotedWidgetSlot) &&
|
||||
!(copies as IBaseWidget[]).includes(w)
|
||||
!(stubs as IBaseWidget[]).includes(w)
|
||||
)
|
||||
|
||||
// Track which source widgets are covered by the parsed list
|
||||
const subgraphNode = node as SubgraphNode
|
||||
const covered = new Set<string>()
|
||||
|
||||
// Create PromotedWidgetSlots for all parsed entries.
|
||||
// Legacy `-1` entries are resolved to real IDs via the copies.
|
||||
const newSlots: IBaseWidget[] = parsed.flatMap(([nodeId, widgetName]) => {
|
||||
let resolvedNodeId = nodeId
|
||||
let resolvedWidgetName = widgetName
|
||||
|
||||
// Legacy `-1` entries: resolve via subgraph input wiring
|
||||
if (nodeId === '-1') {
|
||||
const copy = copies.find((w) => w.name === widgetName)
|
||||
if (!copy) return []
|
||||
resolvedNodeId = copy.sourceNodeId
|
||||
resolvedWidgetName = copy.sourceWidgetName
|
||||
const subgraph = subgraphNode.subgraph
|
||||
const inputSlot = subgraph?.inputNode?.slots.find(
|
||||
(s) => s.name === widgetName
|
||||
)
|
||||
if (!inputSlot || !subgraph) return []
|
||||
|
||||
const linkId = inputSlot.linkIds[0]
|
||||
const link = linkId != null ? subgraph.getLink(linkId) : undefined
|
||||
if (!link) return []
|
||||
|
||||
const resolved = link.resolve(subgraph)
|
||||
const inputWidgetName = resolved.input?.widget?.name
|
||||
if (!resolved.inputNode || !inputWidgetName) return []
|
||||
|
||||
resolvedNodeId = String(resolved.inputNode.id)
|
||||
resolvedWidgetName = inputWidgetName
|
||||
}
|
||||
|
||||
covered.add(`${resolvedNodeId}:${resolvedWidgetName}`)
|
||||
return [
|
||||
new PromotedWidgetSlot(
|
||||
node as SubgraphNode,
|
||||
resolvedNodeId,
|
||||
resolvedWidgetName
|
||||
)
|
||||
new PromotedWidgetSlot(subgraphNode, resolvedNodeId, resolvedWidgetName)
|
||||
]
|
||||
})
|
||||
|
||||
// Add PromotedWidgetSlots for any copies not in the parsed list
|
||||
// Add PromotedWidgetSlots for stubs not in the parsed list
|
||||
// (e.g. old workflows that didn't serialize slot-promoted entries)
|
||||
for (const copy of copies) {
|
||||
const key = `${copy.sourceNodeId}:${copy.sourceWidgetName}`
|
||||
for (const stub of stubs) {
|
||||
const key = `${stub.sourceNodeId}:${stub.sourceWidgetName}`
|
||||
if (covered.has(key)) continue
|
||||
newSlots.unshift(
|
||||
new PromotedWidgetSlot(
|
||||
node as SubgraphNode,
|
||||
copy.sourceNodeId,
|
||||
copy.sourceWidgetName
|
||||
subgraphNode,
|
||||
stub.sourceNodeId,
|
||||
stub.sourceWidgetName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
node.widgets.push(...newSlots)
|
||||
|
||||
// Update input._widget references to point to the new PromotedWidgetSlots
|
||||
// instead of the stubs they replaced.
|
||||
for (const input of subgraphNode.inputs) {
|
||||
const oldWidget = input._widget
|
||||
if (
|
||||
!oldWidget ||
|
||||
!('sourceNodeId' in oldWidget) ||
|
||||
!('sourceWidgetName' in oldWidget)
|
||||
)
|
||||
continue
|
||||
|
||||
const sid = String(oldWidget.sourceNodeId)
|
||||
const swn = String(oldWidget.sourceWidgetName)
|
||||
const replacement = newSlots.find(
|
||||
(w) =>
|
||||
w instanceof PromotedWidgetSlot &&
|
||||
w.sourceNodeId === sid &&
|
||||
w.sourceWidgetName === swn
|
||||
)
|
||||
if (replacement) input._widget = replacement
|
||||
}
|
||||
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
node._setConcreteSlots()
|
||||
node.arrange()
|
||||
|
||||
@@ -29,8 +29,6 @@ import type {
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
||||
import { AssetWidget } from '@/lib/litegraph/src/widgets/AssetWidget'
|
||||
|
||||
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
|
||||
import type { ExecutableLGraphNode, ExecutionId } from './ExecutableNodeDTO'
|
||||
@@ -354,40 +352,19 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
inputWidget: IWidgetLocator | undefined,
|
||||
interiorNode: LGraphNode
|
||||
) {
|
||||
// Use the first matching widget
|
||||
const promotedWidget =
|
||||
widget instanceof BaseWidget
|
||||
? widget.createCopyForNode(this)
|
||||
: { ...widget, node: this }
|
||||
if (widget instanceof AssetWidget)
|
||||
promotedWidget.options.nodeType ??= widget.node.type
|
||||
|
||||
// Delegate value to the interior widget so both the SubgraphNode copy
|
||||
// and the interior widget share the same store entry.
|
||||
// Object.defineProperty is required — Object.assign invokes getters
|
||||
// and copies the result as a data property.
|
||||
const sourceWidget = widget as IBaseWidget
|
||||
Object.defineProperties(promotedWidget, {
|
||||
sourceNodeId: {
|
||||
value: String(interiorNode.id),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
sourceWidgetName: {
|
||||
value: sourceWidget.name,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
const stub: IBaseWidget = Object.create(null)
|
||||
|
||||
Object.defineProperties(stub, {
|
||||
sourceNodeId: { value: String(interiorNode.id), enumerable: true },
|
||||
sourceWidgetName: { value: sourceWidget.name, enumerable: true },
|
||||
node: { value: this, enumerable: true },
|
||||
name: {
|
||||
get: () => subgraphInput.name,
|
||||
set() {},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
localized_name: {
|
||||
get: () => subgraphInput.localized_name,
|
||||
set() {},
|
||||
configurable: true,
|
||||
type: {
|
||||
get: () => sourceWidget.type,
|
||||
enumerable: true
|
||||
},
|
||||
value: {
|
||||
@@ -395,29 +372,29 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
set: (v) => {
|
||||
sourceWidget.value = v
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
options: {
|
||||
get: () => sourceWidget.options,
|
||||
enumerable: true
|
||||
},
|
||||
label: {
|
||||
get: () => subgraphInput.label,
|
||||
set() {},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
tooltip: {
|
||||
get: () => widget.tooltip,
|
||||
set() {},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
})
|
||||
|
||||
const widgetCount = this.inputs.filter((i) => i.widget).length
|
||||
this.widgets.splice(widgetCount, 0, promotedWidget)
|
||||
this.widgets.splice(widgetCount, 0, stub)
|
||||
|
||||
// Dispatch widget-promoted event
|
||||
this.subgraph.events.dispatch('widget-promoted', {
|
||||
widget: promotedWidget,
|
||||
widget: stub,
|
||||
subgraphNode: this
|
||||
})
|
||||
|
||||
@@ -428,7 +405,20 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
input.widget.name = subgraphInput.name
|
||||
if (inputWidget) Object.setPrototypeOf(input.widget, inputWidget)
|
||||
|
||||
input._widget = promotedWidget
|
||||
input._widget = stub
|
||||
|
||||
// Trigger promoted widget slot sync for live connections.
|
||||
// During configure, the proxyWidgets setter isn't defined yet (no-op).
|
||||
// After configure, re-assigning triggers syncPromotedWidgets which
|
||||
// replaces this stub with a proper PromotedWidgetSlot and patches
|
||||
// input._widget references.
|
||||
const desc = Object.getOwnPropertyDescriptor(
|
||||
this.properties,
|
||||
'proxyWidgets'
|
||||
)
|
||||
if (desc?.set) {
|
||||
this.properties.proxyWidgets = this.properties.proxyWidgets
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -585,9 +575,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
// Clean up all subgraph event listeners
|
||||
this._eventAbortController.abort()
|
||||
|
||||
// Clean up all promoted widgets (skip PromotedWidgetSlot instances)
|
||||
// Dispatch widget-demoted for all widgets so listeners can clean up
|
||||
for (const widget of this.widgets) {
|
||||
if (widget.isPromotedSlot) continue
|
||||
this.subgraph.events.dispatch('widget-demoted', {
|
||||
widget,
|
||||
subgraphNode: this
|
||||
|
||||
Reference in New Issue
Block a user