Skip to content

Commit 6566acb

Browse files
authored
Bundled subgraph fixes (#4964)
### Group support for subgraph unpacking The unpacking code would silently delete groups (the cosmetic colored rectangles). They are now correctly transferred. ### Fix subgraph node position on conversion to subgraph Converting to subgraph will no longer cause nodes to inch upwards ![subgraph-conversion-positioning](https://github.com/user-attachments/assets/e120c3f9-5602-4dba-9075-c1eadb534f9a) ### Make unpacking use same positioning calcs as conversion Non trivial, but unpacking is now a proper inverse for conversion. ![subgraph-conversion-inverse](https://github.com/user-attachments/assets/4fcaffca-1c97-4d71-93f7-1af569b1c941) ### Clean up old output links when unpacking Unpacked nodes were left with dangling outputs. This would cause cascading issues later, such as when consecutively unpacking nested subgraphs. ### Minor refactoring for code clarity ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-4964-Bundled-subgraph-fixes-24e6d73d365081d3a043ef1531d9d38a) by [Unito](https://www.unito.io)
1 parent efc0431 commit 6566acb

File tree

2 files changed

+94
-69
lines changed

2 files changed

+94
-69
lines changed

src/lib/litegraph/src/LGraph.ts

Lines changed: 90 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { SubgraphEventMap } from './infrastructure/SubgraphEventMap'
2020
import type {
2121
DefaultConnectionColors,
2222
Dictionary,
23+
HasBoundingRect,
2324
IContextMenuValue,
2425
INodeInputSlot,
2526
INodeOutputSlot,
@@ -28,7 +29,8 @@ import type {
2829
MethodNames,
2930
OptionalProps,
3031
Point,
31-
Positionable
32+
Positionable,
33+
Size
3234
} from './interfaces'
3335
import { LiteGraph, SubgraphNode } from './litegraph'
3436
import {
@@ -1569,6 +1571,9 @@ export class LGraph
15691571
boundingRect
15701572
)
15711573

1574+
//Correct for title height. It's included in bounding box, but not _posSize
1575+
subgraphNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2
1576+
15721577
// Add the subgraph node to the graph
15731578
this.add(subgraphNode)
15741579

@@ -1668,14 +1673,21 @@ export class LGraph
16681673
if (!(subgraphNode instanceof SubgraphNode))
16691674
throw new Error('Can only unpack Subgraph Nodes')
16701675
this.beforeChange()
1671-
const center = [0, 0]
1672-
for (const node of subgraphNode.subgraph.nodes) {
1673-
center[0] += node.pos[0] + node.size[0] / 2
1674-
center[1] += node.pos[1] + node.size[1] / 2
1675-
}
1676-
center[0] /= subgraphNode.subgraph.nodes.length
1677-
center[1] /= subgraphNode.subgraph.nodes.length
1676+
//NOTE: Create bounds can not be called on positionables directly as the subgraph is not being displayed and boundingRect is not initialized.
1677+
//NOTE: NODE_TITLE_HEIGHT is explicitly excluded here
1678+
const positionables = [
1679+
...subgraphNode.subgraph.nodes,
1680+
...subgraphNode.subgraph.reroutes.values(),
1681+
...subgraphNode.subgraph.groups
1682+
].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
1683+
return {
1684+
boundingRect: [p.pos[0], p.pos[1], p.size?.[0] ?? 0, p.size?.[1] ?? 0]
1685+
}
1686+
})
1687+
const bounds = createBounds(positionables) ?? [0, 0, 0, 0]
1688+
const center = [bounds[0] + bounds[2] / 2, bounds[1] + bounds[3] / 2]
16781689

1690+
const toSelect: Positionable[] = []
16791691
const offsetX = subgraphNode.pos[0] - center[0] + subgraphNode.size[0] / 2
16801692
const offsetY = subgraphNode.pos[1] - center[1] + subgraphNode.size[1] / 2
16811693
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
@@ -1697,6 +1709,21 @@ export class LGraph
16971709
for (const input of node.inputs) {
16981710
input.link = null
16991711
}
1712+
for (const output of node.outputs) {
1713+
output.links = []
1714+
}
1715+
toSelect.push(node)
1716+
}
1717+
const groups = structuredClone(
1718+
[...subgraphNode.subgraph.groups].map((g) => g.serialize())
1719+
)
1720+
for (const g_info of groups) {
1721+
const group = new LGraphGroup(g_info.title, g_info.id)
1722+
this.add(group, true)
1723+
group.configure(g_info)
1724+
group.pos[0] += offsetX
1725+
group.pos[1] += offsetY
1726+
toSelect.push(group)
17001727
}
17011728
//cleanup reoute.linkIds now, but leave link.parentIds dangling
17021729
for (const islot of subgraphNode.inputs) {
@@ -1722,17 +1749,16 @@ export class LGraph
17221749
}
17231750
}
17241751
}
1725-
1726-
const newLinks: [
1727-
NodeId,
1728-
number,
1729-
NodeId,
1730-
number,
1731-
LinkId,
1732-
RerouteId | undefined,
1733-
RerouteId | undefined,
1734-
boolean
1735-
][] = []
1752+
const newLinks: {
1753+
oid: NodeId
1754+
oslot: number
1755+
tid: NodeId
1756+
tslot: number
1757+
id: LinkId
1758+
iparent?: RerouteId
1759+
eparent?: RerouteId
1760+
externalFirst: boolean
1761+
}[] = []
17361762
for (const [, link] of subgraphNode.subgraph._links) {
17371763
let externalParentId: RerouteId | undefined
17381764
if (link.origin_id === SUBGRAPH_INPUT_ID) {
@@ -1757,16 +1783,16 @@ export class LGraph
17571783
for (const linkId of subgraphNode.outputs[link.target_slot].links ??
17581784
[]) {
17591785
const sublink = this.links[linkId]
1760-
newLinks.push([
1761-
link.origin_id,
1762-
link.origin_slot,
1763-
sublink.target_id,
1764-
sublink.target_slot,
1765-
link.id,
1766-
link.parentId,
1767-
sublink.parentId,
1768-
true
1769-
])
1786+
newLinks.push({
1787+
oid: link.origin_id,
1788+
oslot: link.origin_slot,
1789+
tid: sublink.target_id,
1790+
tslot: sublink.target_slot,
1791+
id: link.id,
1792+
iparent: link.parentId,
1793+
eparent: sublink.parentId,
1794+
externalFirst: true
1795+
})
17701796
sublink.parentId = undefined
17711797
}
17721798
continue
@@ -1778,60 +1804,60 @@ export class LGraph
17781804
}
17791805
link.target_id = target_id
17801806
}
1781-
newLinks.push([
1782-
link.origin_id,
1783-
link.origin_slot,
1784-
link.target_id,
1785-
link.target_slot,
1786-
link.id,
1787-
link.parentId,
1788-
externalParentId,
1789-
false
1790-
])
1807+
newLinks.push({
1808+
oid: link.origin_id,
1809+
oslot: link.origin_slot,
1810+
tid: link.target_id,
1811+
tslot: link.target_slot,
1812+
id: link.id,
1813+
iparent: link.parentId,
1814+
eparent: externalParentId,
1815+
externalFirst: false
1816+
})
17911817
}
17921818
this.remove(subgraphNode)
17931819
this.subgraphs.delete(subgraphNode.subgraph.id)
17941820
const linkIdMap = new Map<LinkId, LinkId[]>()
17951821
for (const newLink of newLinks) {
17961822
let created: LLink | null | undefined
1797-
if (newLink[0] == SUBGRAPH_INPUT_ID) {
1823+
if (newLink.oid == SUBGRAPH_INPUT_ID) {
17981824
if (!(this instanceof Subgraph)) {
17991825
console.error('Ignoring link to subgraph outside subgraph')
18001826
continue
18011827
}
1802-
const tnode = this._nodes_by_id[newLink[2]]
1803-
created = this.inputNode.slots[newLink[1]].connect(
1804-
tnode.inputs[newLink[3]],
1828+
const tnode = this._nodes_by_id[newLink.tid]
1829+
created = this.inputNode.slots[newLink.oslot].connect(
1830+
tnode.inputs[newLink.tslot],
18051831
tnode
18061832
)
1807-
} else if (newLink[2] == SUBGRAPH_OUTPUT_ID) {
1833+
} else if (newLink.tid == SUBGRAPH_OUTPUT_ID) {
18081834
if (!(this instanceof Subgraph)) {
18091835
console.error('Ignoring link to subgraph outside subgraph')
18101836
continue
18111837
}
1812-
const tnode = this._nodes_by_id[newLink[0]]
1813-
created = this.outputNode.slots[newLink[3]].connect(
1814-
tnode.outputs[newLink[1]],
1838+
const tnode = this._nodes_by_id[newLink.oid]
1839+
created = this.outputNode.slots[newLink.tslot].connect(
1840+
tnode.outputs[newLink.oslot],
18151841
tnode
18161842
)
18171843
} else {
1818-
created = this._nodes_by_id[newLink[0]].connect(
1819-
newLink[1],
1820-
this._nodes_by_id[newLink[2]],
1821-
newLink[3]
1844+
created = this._nodes_by_id[newLink.oid].connect(
1845+
newLink.oslot,
1846+
this._nodes_by_id[newLink.tid],
1847+
newLink.tslot
18221848
)
18231849
}
18241850
if (!created) {
18251851
console.error('Failed to create link')
18261852
continue
18271853
}
18281854
//This is a little unwieldy since Map.has isn't a type guard
1829-
const linkIds = linkIdMap.get(newLink[4]) ?? []
1855+
const linkIds = linkIdMap.get(newLink.id) ?? []
18301856
linkIds.push(created.id)
1831-
if (!linkIdMap.has(newLink[4])) {
1832-
linkIdMap.set(newLink[4], linkIds)
1857+
if (!linkIdMap.has(newLink.id)) {
1858+
linkIdMap.set(newLink.id, linkIds)
18331859
}
1834-
newLink[4] = created.id
1860+
newLink.id = created.id
18351861
}
18361862
const rerouteIdMap = new Map<RerouteId, RerouteId>()
18371863
for (const reroute of subgraphNode.subgraph.reroutes.values()) {
@@ -1847,17 +1873,18 @@ export class LGraph
18471873
])
18481874
rerouteIdMap.set(reroute.id, migratedReroute.id)
18491875
this.reroutes.set(migratedReroute.id, migratedReroute)
1876+
toSelect.push(migratedReroute)
18501877
}
18511878
//iterate over newly created links to update reroute parentIds
18521879
for (const newLink of newLinks) {
1853-
const linkInstance = this.links.get(newLink[4])
1880+
const linkInstance = this.links.get(newLink.id)
18541881
if (!linkInstance) {
18551882
continue
18561883
}
18571884
let instance: Reroute | LLink | undefined = linkInstance
1858-
let parentId: RerouteId | undefined = newLink[6]
1859-
if (newLink[7]) {
1860-
parentId = newLink[6]
1885+
let parentId: RerouteId | undefined = undefined
1886+
if (newLink.externalFirst) {
1887+
parentId = newLink.eparent
18611888
//TODO: recursion check/helper method? Probably exists, but wouldn't mesh with the reference tracking used by this implementation
18621889
while (parentId) {
18631890
instance.parentId = parentId
@@ -1869,7 +1896,7 @@ export class LGraph
18691896
parentId = instance.parentId
18701897
}
18711898
}
1872-
parentId = newLink[5]
1899+
parentId = newLink.iparent
18731900
while (parentId) {
18741901
const migratedId = rerouteIdMap.get(parentId)
18751902
if (!migratedId) throw new Error('Broken Id link when unpacking')
@@ -1883,8 +1910,8 @@ export class LGraph
18831910
if (!oldReroute) throw new Error('Broken Id link when unpacking')
18841911
parentId = oldReroute.parentId
18851912
}
1886-
if (!newLink[7]) {
1887-
parentId = newLink[6]
1913+
if (!newLink.externalFirst) {
1914+
parentId = newLink.eparent
18881915
while (parentId) {
18891916
instance.parentId = parentId
18901917
instance = this.reroutes.get(parentId)
@@ -1897,18 +1924,13 @@ export class LGraph
18971924
}
18981925
}
18991926

1900-
const nodes: LGraphNode[] = []
19011927
for (const nodeId of nodeIdMap.values()) {
19021928
const node = this._nodes_by_id[nodeId]
1903-
nodes.push(node)
19041929
node._setConcreteSlots()
19051930
node.arrange()
19061931
}
1907-
const reroutes = [...rerouteIdMap.values()]
1908-
.map((i) => this.reroutes.get(i))
1909-
.filter((x): x is Reroute => !!x)
19101932

1911-
this.canvasAction((c) => c.selectItems([...nodes, ...reroutes]))
1933+
this.canvasAction((c) => c.selectItems(toSelect))
19121934
this.afterChange()
19131935
}
19141936

src/lib/litegraph/test/subgraph/SubgraphConversion.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { assert, describe, expect, it } from 'vitest'
33
import {
44
ISlotType,
55
LGraph,
6+
LGraphGroup,
67
LGraphNode,
78
LiteGraph
89
} from '@/lib/litegraph/src/litegraph'
@@ -80,7 +81,7 @@ describe('SubgraphConversion', () => {
8081
expect(graph.nodes.length).toBe(4)
8182
expect(graph.links.size).toBe(2)
8283
})
83-
it('Should keep reroutes', () => {
84+
it('Should keep reroutes and groups', () => {
8485
const subgraph = createTestSubgraph({
8586
outputs: [{ name: 'value', type: 'number' }]
8687
})
@@ -98,13 +99,15 @@ describe('SubgraphConversion', () => {
9899
const outer = createNode(graph, ['number'])
99100
const outerLink = subgraphNode.connect(0, outer, 0)
100101
assert(outerLink)
102+
subgraph.add(new LGraphGroup())
101103

102104
subgraph.createReroute([10, 10], innerLink)
103105
graph.createReroute([10, 10], outerLink)
104106

105107
graph.unpackSubgraph(subgraphNode)
106108

107109
expect(graph.reroutes.size).toBe(2)
110+
expect(graph.groups.length).toBe(1)
108111
})
109112
it('Should map reroutes onto split outputs', () => {
110113
const subgraph = createTestSubgraph({

0 commit comments

Comments
 (0)