From ec06e28af7b32550bde7c195aad58ef893f80b91 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Fri, 12 Sep 2025 09:42:08 -0500 Subject: [PATCH] Pull proxyWidget code out of SubgraphNode This was needed from an organizational standpoint. For now, it requires an ugly setTimeout to prevent proxyWidgets from being clobbered during initialization, but this will be cleaned up later. This also allows for the proxy widget code to have type checks ignored. I fully intend to find a functional solution here, but this provides a migration path where typechecking can be enabled for the rest of the PR first Also cleans up type checking on graph change in scripts/app.ts --- src/extensions/core/index.ts | 1 + src/extensions/core/proxyWidget.ts | 118 ++++++++++++++++++ .../litegraph/src/subgraph/SubgraphNode.ts | 102 +-------------- src/scripts/app.ts | 15 ++- 4 files changed, 129 insertions(+), 107 deletions(-) create mode 100644 src/extensions/core/proxyWidget.ts diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 5354ef4e9..00c925935 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -11,6 +11,7 @@ import './maskeditor' import './nodeTemplates' import './noteNode' import './previewAny' +import './proxyWidget' import './rerouteNode' import './saveImageExtraOutput' import './saveMesh' diff --git a/src/extensions/core/proxyWidget.ts b/src/extensions/core/proxyWidget.ts new file mode 100644 index 000000000..c34bfacdc --- /dev/null +++ b/src/extensions/core/proxyWidget.ts @@ -0,0 +1,118 @@ +// @ts-nocheck +// FIXME: typechecking for proxy system + + +import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' +import { useExtensionService } from '@/services/extensionService' +import { useDomWidgetStore } from '@/stores/domWidgetStore' + +useExtensionService().registerExtension({ + name: "Comfy.SubgraphProxyWidgets", + nodeCreated(node: LGraphNode) { + if (node instanceof SubgraphNode) { + setTimeout(() => injectProperty(node),0) + node.addProxyWidget = (nodeId, widgetName) => + addProxyWidget(node, nodeId, widgetName) + } + } +}) +function injectProperty(subgraphNode: SubgraphNode) { + subgraphNode.properties.proxyWidgets ??= [] + const proxyWidgets = subgraphNode.properties.proxyWidgets + Object.defineProperty(subgraphNode.properties, 'proxyWidgets', { + get: () => { + return subgraphNode.widgets + .filter((w) => !!w._overlay) + .map((w) => [w._overlay.nodeId, w._overlay.widgetName]) + }, + set: (property) => { + const { widgetStates } = useDomWidgetStore() + subgraphNode.widgets.forEach((w) => { + if (w.id && widgetStates.has(w.id)) + widgetStates.get(w.id).active = false + }) + //NOTE: This does not apply to pushed entries, only initial load + subgraphNode.widgets = subgraphNode.widgets.filter((w) => !w._overlay) + for (const [nodeId, widgetName] of property) { + const w = addProxyWidget(subgraphNode, `${nodeId}`, widgetName) + if (w.id && widgetStates.has(w.id)) { + const widgetState = widgetStates.get(w.id) + widgetState.active = true + widgetState.widget = w + } + } + //TODO: set dirty canvas + } + }) + subgraphNode.properties.proxyWidgets = proxyWidgets +} + +function addProxyWidget(subgraphNode: SubgraphNode, nodeId: string, widgetName: string) { + const overlay = { nodeId, widgetName } + return addProxyFromOverlay(subgraphNode, { __proto__: overlay }) +} +function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: object) { + overlay.label = `${overlay.nodeId}: ${overlay.widgetName}` + overlay.graph = subgraphNode.subgraph + overlay.isProxyWidget = true + //TODO: Add minimal caching for linkedWidget? + //use a weakref and only trigger recalc on calls when undefined? + //TODO: call toConcrete when resolved and hold reference? + function linkedWidget(graph, nodeId = '', widgetName) { + const g = graph + let n = undefined + for (const id of nodeId.split(':')) { + n = g?._nodes_by_id?.[id] + graph = n?.subgraph + } + if (!n) return + return n.widgets.find((w) => w.name === widgetName) + } + let lw = undefined + const handler = Object.fromEntries( + ['get', 'set', 'getPrototypeOf', 'ownKeys', 'has'].map((s) => { + const func = function (t, p, ...rest) { + if (s == 'get' && p == '_overlay') return overlay + if (!lw) { + lw = linkedWidget(overlay.graph, overlay.nodeId, overlay.widgetName) + } + if (s == 'get' && p == 'node') { + return subgraphNode + } + if (s == 'set' && p == 'computedDisabled') { + //ignore setting, calc actual + lw.computedDisabled = + lw.disabled || lw.node.getSlotFromWidget(lw)?.link != null + return true + } + //NOTE: p may be undefined + let r = rest.at(-1) + if ( + [ + 'y', + 'last_y', + 'width', + 'computedHeight', + 'afterQueued', + 'beforeQueued', + 'onRemove', + 'isProxyWidget', + 'label' + ].includes(p) + ) + t = overlay + else { + t = lw + if (!t) t = { __proto__: overlay, draw: drawDisconnected } + if (p == 'value') r = t + } + return Reflect[s](t, p, ...rest.slice(0, -1), r) + } + return [s, func] + }) + ) + const w = new Proxy(overlay, handler) + subgraphNode.widgets.push(w) + return w +} diff --git a/src/lib/litegraph/src/subgraph/SubgraphNode.ts b/src/lib/litegraph/src/subgraph/SubgraphNode.ts index 11c2bb134..d87e49658 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphNode.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphNode.ts @@ -23,7 +23,6 @@ import type { import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import type { UUID } from '@/lib/litegraph/src/utils/uuid' import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap' -import { useDomWidgetStore } from '@/stores/domWidgetStore' import { type ExecutableLGraphNode, @@ -149,8 +148,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { ) this.type = subgraph.id - //FIXME: This breaks subgraph conversion - //this.configure(instanceData) + this.configure(instanceData) this.addTitleButton({ name: 'enter_subgraph', @@ -307,35 +305,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { break } } - //ensure proxyWidgets is enumerated during serialization - this.properties.proxyWidgets ??= [] - const proxyWidgets = this.properties.proxyWidgets - Object.defineProperty(this.properties, 'proxyWidgets', { - get: () => { - return this.widgets - .filter((w) => !!w._overlay) - .map((w) => [w._overlay.nodeId, w._overlay.widgetName]) - }, - set: (property) => { - const { widgetStates } = useDomWidgetStore() - this.widgets.forEach((w) => { - if (w.id && widgetStates.has(w.id)) - widgetStates.get(w.id).active = false - }) - //NOTE: This does not apply to pushed entries, only initial load - this.widgets = this.widgets.filter((w) => !w._overlay) - for (const [nodeId, widgetName] of property) { - const w = this.addProxyWidget(`${nodeId}`, widgetName) - if (w.id && widgetStates.has(w.id)) { - const widgetState = widgetStates.get(w.id) - widgetState.active = true - widgetState.widget = w - } - } - //TODO: set dirty canvas - } - }) - this.properties.proxyWidgets = proxyWidgets } #setWidget( @@ -607,73 +576,4 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { // Call parent serialize method return super.serialize() } - addProxyWidget(nodeId: string, widgetName: string) { - const overlay = { nodeId, widgetName } - return this.addProxyFromOverlay({ __proto__: overlay }) - } - addProxyFromOverlay(overlay: object) { - overlay.label = `${overlay.nodeId}: ${overlay.widgetName}` - overlay.graph = this.subgraph - overlay.isProxyWidget = true - //TODO: Add minimal caching for linkedWidget? - //use a weakref and only trigger recalc on calls when undefined? - //TODO: call toConcrete when resolved and hold reference? - const subgraphNode = this - function linkedWidget(graph, nodeId = '', widgetName) { - const g = graph - let n = undefined - for (const id of nodeId.split(':')) { - n = g?._nodes_by_id?.[id] - graph = n?.subgraph - } - if (!n) return - return n.widgets.find((w) => w.name === widgetName) - } - let lw = undefined - const handler = Object.fromEntries( - ['get', 'set', 'getPrototypeOf', 'ownKeys', 'has'].map((s) => { - const func = function (t, p, ...rest) { - if (s == 'get' && p == '_overlay') return overlay - if (!lw) { - lw = linkedWidget(overlay.graph, overlay.nodeId, overlay.widgetName) - } - if (s == 'get' && p == 'node') { - return subgraphNode - } - if (s == 'set' && p == 'computedDisabled') { - //ignore setting, calc actual - lw.computedDisabled = - lw.disabled || lw.node.getSlotFromWidget(lw)?.link != null - return true - } - //NOTE: p may be undefined - let r = rest.at(-1) - if ( - [ - 'y', - 'last_y', - 'width', - 'computedHeight', - 'afterQueued', - 'beforeQueued', - 'onRemove', - 'isProxyWidget', - 'label' - ].includes(p) - ) - t = overlay - else { - t = lw - if (!t) t = { __proto__: overlay, draw: drawDisconnected } - if (p == 'value') r = t - } - return Reflect[s](t, p, ...rest.slice(0, -1), r) - } - return [s, func] - }) - ) - const w = new Proxy(overlay, handler) - this.widgets.push(w) - return w - } } diff --git a/src/scripts/app.ts b/src/scripts/app.ts index ade027255..dd897cfaf 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -31,6 +31,7 @@ import { isComboInputSpecV1, isComboInputSpecV2 } from '@/schemas/nodeDefSchema' +import { type BaseDOMWidget, DOMWidgetImpl } from '@/scripts/domWidget' import { getFromWebmFile } from '@/scripts/metadata/ebml' import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf' import { getFromIsobmffFile } from '@/scripts/metadata/isobmff' @@ -839,19 +840,21 @@ export class ComfyApp { (e) => { // Assertion: Not yet defined in litegraph. const { newGraph } = e.detail - - const widgetIds = {} + + const widgetIds: Record> = {} const widgetStore = useDomWidgetStore() - + for (const node of newGraph.nodes) for (const w of node.widgets ?? []) - if (w.id) + if (w instanceof DOMWidgetImpl && w.id) widgetIds[w.id] = w - + // Assertions: UnwrapRef for (const widgetId of widgetStore.widgetStates.keys()) { const widgetState = widgetStore - .widgetStates.get(widgetId) + .widgetStates.get(widgetId) + //Unreachable, but required for type safety + if (!widgetState) continue if (widgetId in widgetIds) { widgetState.active = true widgetState.widget = widgetIds[widgetId]