Skip to content

Commit 6f068c8

Browse files
authored
Multiple fixes related to copying subgraphs (#6383)
- DOMWidgets have ownership reset after a `subgraphNode.clone()`. - Fixes Ctrl+C on a subgraphNode with a prompted prompt making the prompt disappear. - alt + drag uses the copy/paste pathway that deeply clones subgraphs. - Fixed dangling references on nodes in subgraphs by updating subgraph ids before configuration. - Attempt to recursively resolve disconnected proxyWidgets (Can matter when subgraphs load out of order). - Fix Right click -> clone creating linked copies of subgraphs. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6383-Multiple-fixes-related-to-copying-subgraphs-29b6d73d365081819671ced440dde327) by [Unito](https://www.unito.io)
1 parent 86c0fb1 commit 6f068c8

File tree

3 files changed

+64
-88
lines changed

3 files changed

+64
-88
lines changed

src/core/graph/subgraph/proxyWidget.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ function resolveLinkedWidget(
158158
const { graph, nodeId, widgetName } = overlay
159159
const n = getNodeByExecutionId(graph, nodeId)
160160
if (!n) return [undefined, undefined]
161-
return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)]
161+
const widget = n.widgets?.find((w: IBaseWidget) => w.name === widgetName)
162+
//Slightly hacky. Force recursive resolution of nested widgets
163+
if (widget instanceof disconnectedWidget.constructor && isProxyWidget(widget))
164+
widget.computedHeight = 20
165+
return [n, widget]
162166
}
163167

164168
function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {

src/lib/litegraph/src/LGraphCanvas.ts

Lines changed: 46 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ import {
8686
RenderShape,
8787
TitleMode
8888
} from './types/globalEnums'
89-
import type { ClipboardItems, SubgraphIO } from './types/serialisation'
89+
import type {
90+
ClipboardItems,
91+
ISerialisedNode,
92+
SubgraphIO
93+
} from './types/serialisation'
9094
import type { NeverNever, PickNevers } from './types/utility'
9195
import type { IBaseWidget } from './types/widgets'
9296
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
@@ -1772,47 +1776,24 @@ export class LGraphCanvas
17721776
menu: ContextMenu,
17731777
node: LGraphNode
17741778
): void {
1775-
const { graph } = node
1776-
if (!graph) throw new NullGraphError()
1777-
graph.beforeChange()
1778-
1779-
const newSelected = new Set<LGraphNode>()
1780-
1781-
const fApplyMultiNode = function (
1782-
node: LGraphNode,
1783-
newNodes: Set<LGraphNode>
1784-
): void {
1785-
if (node.clonable === false) return
1786-
1787-
const newnode = node.clone()
1788-
if (!newnode) return
1789-
1790-
newnode.pos = [node.pos[0] + 5, node.pos[1] + 5]
1791-
if (!node.graph) throw new NullGraphError()
1792-
1793-
node.graph.add(newnode)
1794-
newNodes.add(newnode)
1795-
}
1796-
17971779
const canvas = LGraphCanvas.active_canvas
1798-
if (
1799-
!canvas.selected_nodes ||
1800-
Object.keys(canvas.selected_nodes).length <= 1
1801-
) {
1802-
fApplyMultiNode(node, newSelected)
1803-
} else {
1804-
for (const i in canvas.selected_nodes) {
1805-
fApplyMultiNode(canvas.selected_nodes[i], newSelected)
1806-
}
1807-
}
1780+
const nodes = canvas.selectedItems.size ? canvas.selectedItems : [node]
18081781

1809-
if (newSelected.size) {
1810-
canvas.selectNodes([...newSelected])
1782+
// Find top-left-most boundary
1783+
let offsetX = Infinity
1784+
let offsetY = Infinity
1785+
for (const item of nodes) {
1786+
if (item.pos == null)
1787+
throw new TypeError(
1788+
'Invalid node encountered on clone. `pos` was null.'
1789+
)
1790+
if (item.pos[0] < offsetX) offsetX = item.pos[0]
1791+
if (item.pos[1] < offsetY) offsetY = item.pos[1]
18111792
}
18121793

1813-
graph.afterChange()
1814-
1815-
canvas.setDirty(true, true)
1794+
canvas._deserializeItems(canvas._serializeItems(nodes), {
1795+
position: [offsetX + 5, offsetY + 5]
1796+
})
18161797
}
18171798

18181799
/**
@@ -2384,42 +2365,22 @@ export class LGraphCanvas
23842365
node &&
23852366
this.allow_interaction
23862367
) {
2387-
let newType = node.type
2388-
2389-
if (node instanceof SubgraphNode) {
2390-
const cloned = node.subgraph.clone().asSerialisable()
2391-
2392-
const subgraph = graph.createSubgraph(cloned)
2393-
subgraph.configure(cloned)
2394-
newType = subgraph.id
2395-
}
2396-
2397-
const node_data = node.clone()?.serialize()
2398-
if (node_data?.type != null) {
2399-
// Ensure the cloned node is configured against the correct type (especially for SubgraphNodes)
2400-
node_data.type = newType
2401-
const cloned = LiteGraph.createNode(newType)
2402-
if (cloned) {
2403-
cloned.configure(node_data)
2404-
cloned.pos[0] += 5
2405-
cloned.pos[1] += 5
2368+
const items = this._deserializeItems(this._serializeItems([node]), {
2369+
position: node.pos
2370+
})
2371+
const cloned = items?.created[0] as LGraphNode | undefined
2372+
if (!cloned) return
24062373

2407-
if (this.allow_dragnodes) {
2408-
pointer.onDragStart = (pointer) => {
2409-
graph.add(cloned, false)
2410-
this.#startDraggingItems(cloned, pointer)
2411-
}
2412-
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
2413-
} else {
2414-
// TODO: Check if before/after change are necessary here.
2415-
graph.beforeChange()
2416-
graph.add(cloned, false)
2417-
graph.afterChange()
2418-
}
2374+
cloned.pos[0] += 5
2375+
cloned.pos[1] += 5
24192376

2420-
return
2377+
if (this.allow_dragnodes) {
2378+
pointer.onDragStart = (pointer) => {
2379+
this.#startDraggingItems(cloned, pointer)
24212380
}
2381+
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
24222382
}
2383+
return
24232384
}
24242385

24252386
// Node clicked
@@ -3963,17 +3924,26 @@ export class LGraphCanvas
39633924
const { created, nodes, links, reroutes } = results
39643925

39653926
// const failedNodes: ISerialisedNode[] = []
3927+
const subgraphIdMap: Record<string, string> = {}
3928+
// SubgraphV2: Remove always-clone behaviour
3929+
//Update subgraph ids
3930+
for (const subgraphInfo of parsed.subgraphs)
3931+
subgraphInfo.id = subgraphIdMap[subgraphInfo.id] = createUuidv4()
3932+
const allNodeInfo: ISerialisedNode[] = [
3933+
parsed.nodes ? [parsed.nodes] : [],
3934+
parsed.subgraphs ? parsed.subgraphs.map((s) => s.nodes ?? []) : []
3935+
].flat(2)
3936+
for (const nodeInfo of allNodeInfo)
3937+
if (nodeInfo.type in subgraphIdMap)
3938+
nodeInfo.type = subgraphIdMap[nodeInfo.type]
39663939

39673940
// Subgraphs
39683941
for (const info of parsed.subgraphs) {
3969-
// SubgraphV2: Remove always-clone behaviour
3970-
const originalId = info.id
3971-
info.id = createUuidv4()
3972-
39733942
const subgraph = graph.createSubgraph(info)
3974-
subgraph.configure(info)
3975-
results.subgraphs.set(originalId, subgraph)
3943+
results.subgraphs.set(info.id, subgraph)
39763944
}
3945+
for (const info of parsed.subgraphs)
3946+
results.subgraphs.get(info.id)?.configure(info)
39773947

39783948
// Groups
39793949
for (const info of parsed.groups) {
@@ -3985,17 +3955,6 @@ export class LGraphCanvas
39853955
created.push(group)
39863956
}
39873957

3988-
// Update subgraph ids with nesting
3989-
function updateSubgraphIds(nodes: { type: string }[]) {
3990-
for (const info of nodes) {
3991-
const subgraph = results.subgraphs.get(info.type)
3992-
if (!subgraph) continue
3993-
info.type = subgraph.id
3994-
updateSubgraphIds(subgraph.nodes)
3995-
}
3996-
}
3997-
updateSubgraphIds(parsed.nodes)
3998-
39993958
// Nodes
40003959
for (const info of parsed.nodes) {
40013960
const node = info.type == null ? null : LiteGraph.createNode(info.type)

src/lib/litegraph/src/subgraph/SubgraphNode.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,4 +618,17 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
618618
// Call parent serialize method
619619
return super.serialize()
620620
}
621+
override clone() {
622+
const clone = super.clone()
623+
// force reasign so domWidgets reset ownership
624+
// eslint-disable-next-line no-self-assign
625+
this.properties.proxyWidgets = this.properties.proxyWidgets
626+
627+
//TODO: Consider deep cloning subgraphs here.
628+
//It's the safest place to prevent creation of linked subgraphs
629+
//But the frequency of clone().serialize() calls is likely to result in
630+
//pollution of rootGraph.subgraphs
631+
632+
return clone
633+
}
621634
}

0 commit comments

Comments
 (0)