mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
[backport cloud/1.41] feat: resolveVirtualOutput for cross-subgraph virtual nodes (eg. Set/Get) (#10180)
Backport of #10111 to `cloud/1.41` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10180-backport-cloud-1-41-feat-resolveVirtualOutput-for-cross-subgraph-virtual-nodes-eg-S-3266d73d365081fa9f89d8622b4aac9c) by [Unito](https://www.unito.io) Co-authored-by: Jukka Seppänen <40791699+kijai@users.noreply.github.com>
This commit is contained in:
@@ -1206,6 +1206,14 @@ export class LGraphNode
|
||||
: this.inputs[slot]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the output source for cross-graph virtual nodes (e.g. Set/Get),
|
||||
* bypassing {@link getInputLink} when the source lives in a different graph.
|
||||
*/
|
||||
resolveVirtualOutput?(
|
||||
slot: number
|
||||
): { node: LGraphNode; slot: number } | undefined
|
||||
|
||||
/**
|
||||
* Returns the link info in the connection of an input slot
|
||||
* @returns object or null
|
||||
|
||||
@@ -382,6 +382,102 @@ describe('ALWAYS mode node output resolution', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Virtual node resolveVirtualOutput', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('should resolve through resolveVirtualOutput when implemented', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
const sourceNode = new LGraphNode('Source')
|
||||
sourceNode.addOutput('out', 'IMAGE')
|
||||
graph.add(sourceNode)
|
||||
|
||||
const virtualNode = new LGraphNode('Virtual Get')
|
||||
virtualNode.addOutput('out', 'IMAGE')
|
||||
virtualNode.isVirtualNode = true
|
||||
virtualNode.resolveVirtualOutput = () => ({ node: sourceNode, slot: 0 })
|
||||
graph.add(virtualNode)
|
||||
|
||||
const nodeDtoMap = new Map()
|
||||
const sourceDto = new ExecutableNodeDTO(
|
||||
sourceNode,
|
||||
[],
|
||||
nodeDtoMap,
|
||||
undefined
|
||||
)
|
||||
nodeDtoMap.set(sourceDto.id, sourceDto)
|
||||
|
||||
const virtualDto = new ExecutableNodeDTO(
|
||||
virtualNode,
|
||||
[],
|
||||
nodeDtoMap,
|
||||
undefined
|
||||
)
|
||||
nodeDtoMap.set(virtualDto.id, virtualDto)
|
||||
|
||||
const resolved = virtualDto.resolveOutput(0, 'IMAGE', new Set())
|
||||
expect(resolved).toBeDefined()
|
||||
expect(resolved?.node).toBe(sourceDto)
|
||||
expect(resolved?.origin_slot).toBe(0)
|
||||
})
|
||||
|
||||
it('should throw when resolveVirtualOutput returns a node with no matching DTO', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
const unmappedNode = new LGraphNode('Unmapped Source')
|
||||
unmappedNode.addOutput('out', 'IMAGE')
|
||||
graph.add(unmappedNode)
|
||||
|
||||
const virtualNode = new LGraphNode('Virtual Get')
|
||||
virtualNode.addOutput('out', 'IMAGE')
|
||||
virtualNode.isVirtualNode = true
|
||||
virtualNode.resolveVirtualOutput = () => ({
|
||||
node: unmappedNode,
|
||||
slot: 0
|
||||
})
|
||||
graph.add(virtualNode)
|
||||
|
||||
const nodeDtoMap = new Map()
|
||||
const virtualDto = new ExecutableNodeDTO(
|
||||
virtualNode,
|
||||
[],
|
||||
nodeDtoMap,
|
||||
undefined
|
||||
)
|
||||
nodeDtoMap.set(virtualDto.id, virtualDto)
|
||||
|
||||
expect(() => virtualDto.resolveOutput(0, 'IMAGE', new Set())).toThrow(
|
||||
'No DTO found for virtual source node'
|
||||
)
|
||||
})
|
||||
|
||||
it('should fall through to getInputLink when resolveVirtualOutput returns undefined', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
const virtualNode = new LGraphNode('Virtual Passthrough')
|
||||
virtualNode.addOutput('out', 'IMAGE')
|
||||
virtualNode.isVirtualNode = true
|
||||
virtualNode.resolveVirtualOutput = () => undefined
|
||||
graph.add(virtualNode)
|
||||
|
||||
const nodeDtoMap = new Map()
|
||||
const virtualDto = new ExecutableNodeDTO(
|
||||
virtualNode,
|
||||
[],
|
||||
nodeDtoMap,
|
||||
undefined
|
||||
)
|
||||
nodeDtoMap.set(virtualDto.id, virtualDto)
|
||||
|
||||
const spy = vi.spyOn(virtualNode, 'getInputLink')
|
||||
const resolved = virtualDto.resolveOutput(0, 'IMAGE', new Set())
|
||||
expect(resolved).toBeUndefined()
|
||||
expect(spy).toHaveBeenCalledWith(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('ExecutableNodeDTO Properties', () => {
|
||||
it('should provide access to basic properties', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
@@ -291,6 +291,20 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
return this._resolveSubgraphOutput(slot, type, visited)
|
||||
|
||||
if (node.isVirtualNode) {
|
||||
// Cross-graph virtual nodes (e.g. Set/Get) resolve their source directly.
|
||||
const virtualSource = this.node.resolveVirtualOutput?.(slot)
|
||||
if (virtualSource) {
|
||||
const inputNodeDto = [...this.nodesByExecutionId.values()].find(
|
||||
(dto) =>
|
||||
dto instanceof ExecutableNodeDTO && dto.node === virtualSource.node
|
||||
)
|
||||
if (!inputNodeDto)
|
||||
throw new Error(
|
||||
`No DTO found for virtual source node [${virtualSource.node.id}]`
|
||||
)
|
||||
|
||||
return inputNodeDto.resolveOutput(virtualSource.slot, type, visited)
|
||||
}
|
||||
const virtualLink = this.node.getInputLink(slot)
|
||||
if (virtualLink) {
|
||||
const { inputNode } = virtualLink.resolve(this.graph)
|
||||
|
||||
Reference in New Issue
Block a user