Skip to content

Commit e054d8c

Browse files
AustinMrozgithub-actions[bot]
authored andcommitted
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 3897a75 commit e054d8c

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
@@ -18,6 +18,7 @@ import type { SubgraphEventMap } from './infrastructure/SubgraphEventMap'
1818
import type {
1919
DefaultConnectionColors,
2020
Dictionary,
21+
HasBoundingRect,
2122
IContextMenuValue,
2223
INodeInputSlot,
2324
INodeOutputSlot,
@@ -26,7 +27,8 @@ import type {
2627
MethodNames,
2728
OptionalProps,
2829
Point,
29-
Positionable
30+
Positionable,
31+
Size
3032
} from './interfaces'
3133
import { LiteGraph, SubgraphNode } from './litegraph'
3234
import {
@@ -1568,6 +1570,9 @@ export class LGraph
15681570
boundingRect
15691571
)
15701572

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

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

1689+
const toSelect: Positionable[] = []
16781690
const offsetX = subgraphNode.pos[0] - center[0] + subgraphNode.size[0] / 2
16791691
const offsetY = subgraphNode.pos[1] - center[1] + subgraphNode.size[1] / 2
16801692
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
@@ -1696,6 +1708,21 @@ export class LGraph
16961708
for (const input of node.inputs) {
16971709
input.link = null
16981710
}
1711+
for (const output of node.outputs) {
1712+
output.links = []
1713+
}
1714+
toSelect.push(node)
1715+
}
1716+
const groups = structuredClone(
1717+
[...subgraphNode.subgraph.groups].map((g) => g.serialize())
1718+
)
1719+
for (const g_info of groups) {
1720+
const group = new LGraphGroup(g_info.title, g_info.id)
1721+
this.add(group, true)
1722+
group.configure(g_info)
1723+
group.pos[0] += offsetX
1724+
group.pos[1] += offsetY
1725+
toSelect.push(group)
16991726
}
17001727
//cleanup reoute.linkIds now, but leave link.parentIds dangling
17011728
for (const islot of subgraphNode.inputs) {
@@ -1721,17 +1748,16 @@ export class LGraph
17211748
}
17221749
}
17231750
}
1724-
1725-
const newLinks: [
1726-
NodeId,
1727-
number,
1728-
NodeId,
1729-
number,
1730-
LinkId,
1731-
RerouteId | undefined,
1732-
RerouteId | undefined,
1733-
boolean
1734-
][] = []
1751+
const newLinks: {
1752+
oid: NodeId
1753+
oslot: number
1754+
tid: NodeId
1755+
tslot: number
1756+
id: LinkId
1757+
iparent?: RerouteId
1758+
eparent?: RerouteId
1759+
externalFirst: boolean
1760+
}[] = []
17351761
for (const [, link] of subgraphNode.subgraph._links) {
17361762
let externalParentId: RerouteId | undefined
17371763
if (link.origin_id === SUBGRAPH_INPUT_ID) {
@@ -1756,16 +1782,16 @@ export class LGraph
17561782
for (const linkId of subgraphNode.outputs[link.target_slot].links ??
17571783
[]) {
17581784
const sublink = this.links[linkId]
1759-
newLinks.push([
1760-
link.origin_id,
1761-
link.origin_slot,
1762-
sublink.target_id,
1763-
sublink.target_slot,
1764-
link.id,
1765-
link.parentId,
1766-
sublink.parentId,
1767-
true
1768-
])
1785+
newLinks.push({
1786+
oid: link.origin_id,
1787+
oslot: link.origin_slot,
1788+
tid: sublink.target_id,
1789+
tslot: sublink.target_slot,
1790+
id: link.id,
1791+
iparent: link.parentId,
1792+
eparent: sublink.parentId,
1793+
externalFirst: true
1794+
})
17691795
sublink.parentId = undefined
17701796
}
17711797
continue
@@ -1777,60 +1803,60 @@ export class LGraph
17771803
}
17781804
link.target_id = target_id
17791805
}
1780-
newLinks.push([
1781-
link.origin_id,
1782-
link.origin_slot,
1783-
link.target_id,
1784-
link.target_slot,
1785-
link.id,
1786-
link.parentId,
1787-
externalParentId,
1788-
false
1789-
])
1806+
newLinks.push({
1807+
oid: link.origin_id,
1808+
oslot: link.origin_slot,
1809+
tid: link.target_id,
1810+
tslot: link.target_slot,
1811+
id: link.id,
1812+
iparent: link.parentId,
1813+
eparent: externalParentId,
1814+
externalFirst: false
1815+
})
17901816
}
17911817
this.remove(subgraphNode)
17921818
this.subgraphs.delete(subgraphNode.subgraph.id)
17931819
const linkIdMap = new Map<LinkId, LinkId[]>()
17941820
for (const newLink of newLinks) {
17951821
let created: LLink | null | undefined
1796-
if (newLink[0] == SUBGRAPH_INPUT_ID) {
1822+
if (newLink.oid == SUBGRAPH_INPUT_ID) {
17971823
if (!(this instanceof Subgraph)) {
17981824
console.error('Ignoring link to subgraph outside subgraph')
17991825
continue
18001826
}
1801-
const tnode = this._nodes_by_id[newLink[2]]
1802-
created = this.inputNode.slots[newLink[1]].connect(
1803-
tnode.inputs[newLink[3]],
1827+
const tnode = this._nodes_by_id[newLink.tid]
1828+
created = this.inputNode.slots[newLink.oslot].connect(
1829+
tnode.inputs[newLink.tslot],
18041830
tnode
18051831
)
1806-
} else if (newLink[2] == SUBGRAPH_OUTPUT_ID) {
1832+
} else if (newLink.tid == SUBGRAPH_OUTPUT_ID) {
18071833
if (!(this instanceof Subgraph)) {
18081834
console.error('Ignoring link to subgraph outside subgraph')
18091835
continue
18101836
}
1811-
const tnode = this._nodes_by_id[newLink[0]]
1812-
created = this.outputNode.slots[newLink[3]].connect(
1813-
tnode.outputs[newLink[1]],
1837+
const tnode = this._nodes_by_id[newLink.oid]
1838+
created = this.outputNode.slots[newLink.tslot].connect(
1839+
tnode.outputs[newLink.oslot],
18141840
tnode
18151841
)
18161842
} else {
1817-
created = this._nodes_by_id[newLink[0]].connect(
1818-
newLink[1],
1819-
this._nodes_by_id[newLink[2]],
1820-
newLink[3]
1843+
created = this._nodes_by_id[newLink.oid].connect(
1844+
newLink.oslot,
1845+
this._nodes_by_id[newLink.tid],
1846+
newLink.tslot
18211847
)
18221848
}
18231849
if (!created) {
18241850
console.error('Failed to create link')
18251851
continue
18261852
}
18271853
//This is a little unwieldy since Map.has isn't a type guard
1828-
const linkIds = linkIdMap.get(newLink[4]) ?? []
1854+
const linkIds = linkIdMap.get(newLink.id) ?? []
18291855
linkIds.push(created.id)
1830-
if (!linkIdMap.has(newLink[4])) {
1831-
linkIdMap.set(newLink[4], linkIds)
1856+
if (!linkIdMap.has(newLink.id)) {
1857+
linkIdMap.set(newLink.id, linkIds)
18321858
}
1833-
newLink[4] = created.id
1859+
newLink.id = created.id
18341860
}
18351861
const rerouteIdMap = new Map<RerouteId, RerouteId>()
18361862
for (const reroute of subgraphNode.subgraph.reroutes.values()) {
@@ -1846,17 +1872,18 @@ export class LGraph
18461872
])
18471873
rerouteIdMap.set(reroute.id, migratedReroute.id)
18481874
this.reroutes.set(migratedReroute.id, migratedReroute)
1875+
toSelect.push(migratedReroute)
18491876
}
18501877
//iterate over newly created links to update reroute parentIds
18511878
for (const newLink of newLinks) {
1852-
const linkInstance = this.links.get(newLink[4])
1879+
const linkInstance = this.links.get(newLink.id)
18531880
if (!linkInstance) {
18541881
continue
18551882
}
18561883
let instance: Reroute | LLink | undefined = linkInstance
1857-
let parentId: RerouteId | undefined = newLink[6]
1858-
if (newLink[7]) {
1859-
parentId = newLink[6]
1884+
let parentId: RerouteId | undefined = undefined
1885+
if (newLink.externalFirst) {
1886+
parentId = newLink.eparent
18601887
//TODO: recursion check/helper method? Probably exists, but wouldn't mesh with the reference tracking used by this implementation
18611888
while (parentId) {
18621889
instance.parentId = parentId
@@ -1868,7 +1895,7 @@ export class LGraph
18681895
parentId = instance.parentId
18691896
}
18701897
}
1871-
parentId = newLink[5]
1898+
parentId = newLink.iparent
18721899
while (parentId) {
18731900
const migratedId = rerouteIdMap.get(parentId)
18741901
if (!migratedId) throw new Error('Broken Id link when unpacking')
@@ -1882,8 +1909,8 @@ export class LGraph
18821909
if (!oldReroute) throw new Error('Broken Id link when unpacking')
18831910
parentId = oldReroute.parentId
18841911
}
1885-
if (!newLink[7]) {
1886-
parentId = newLink[6]
1912+
if (!newLink.externalFirst) {
1913+
parentId = newLink.eparent
18871914
while (parentId) {
18881915
instance.parentId = parentId
18891916
instance = this.reroutes.get(parentId)
@@ -1896,18 +1923,13 @@ export class LGraph
18961923
}
18971924
}
18981925

1899-
const nodes: LGraphNode[] = []
19001926
for (const nodeId of nodeIdMap.values()) {
19011927
const node = this._nodes_by_id[nodeId]
1902-
nodes.push(node)
19031928
node._setConcreteSlots()
19041929
node.arrange()
19051930
}
1906-
const reroutes = [...rerouteIdMap.values()]
1907-
.map((i) => this.reroutes.get(i))
1908-
.filter((x): x is Reroute => !!x)
19091931

1910-
this.canvasAction((c) => c.selectItems([...nodes, ...reroutes]))
1932+
this.canvasAction((c) => c.selectItems(toSelect))
19111933
this.afterChange()
19121934
}
19131935

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)