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
This commit is contained in:
Austin Mroz
2025-09-12 09:42:08 -05:00
parent 5d4c4ef63e
commit ec06e28af7
4 changed files with 129 additions and 107 deletions

View File

@@ -11,6 +11,7 @@ import './maskeditor'
import './nodeTemplates'
import './noteNode'
import './previewAny'
import './proxyWidget'
import './rerouteNode'
import './saveImageExtraOutput'
import './saveMesh'

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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<string, BaseDOMWidget<object | string>> = {}
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]