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)
This commit is contained in:
AustinMroz
2025-10-29 18:05:04 -07:00
committed by GitHub
parent 86c0fb11f1
commit 6f068c87da
3 changed files with 65 additions and 89 deletions

View File

@@ -158,7 +158,11 @@ function resolveLinkedWidget(
const { graph, nodeId, widgetName } = overlay
const n = getNodeByExecutionId(graph, nodeId)
if (!n) return [undefined, undefined]
return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)]
const widget = n.widgets?.find((w: IBaseWidget) => w.name === widgetName)
//Slightly hacky. Force recursive resolution of nested widgets
if (widget instanceof disconnectedWidget.constructor && isProxyWidget(widget))
widget.computedHeight = 20
return [n, widget]
}
function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {

View File

@@ -86,7 +86,11 @@ import {
RenderShape,
TitleMode
} from './types/globalEnums'
import type { ClipboardItems, SubgraphIO } from './types/serialisation'
import type {
ClipboardItems,
ISerialisedNode,
SubgraphIO
} from './types/serialisation'
import type { NeverNever, PickNevers } from './types/utility'
import type { IBaseWidget } from './types/widgets'
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
@@ -1772,47 +1776,24 @@ export class LGraphCanvas
menu: ContextMenu,
node: LGraphNode
): void {
const { graph } = node
if (!graph) throw new NullGraphError()
graph.beforeChange()
const newSelected = new Set<LGraphNode>()
const fApplyMultiNode = function (
node: LGraphNode,
newNodes: Set<LGraphNode>
): void {
if (node.clonable === false) return
const newnode = node.clone()
if (!newnode) return
newnode.pos = [node.pos[0] + 5, node.pos[1] + 5]
if (!node.graph) throw new NullGraphError()
node.graph.add(newnode)
newNodes.add(newnode)
}
const canvas = LGraphCanvas.active_canvas
if (
!canvas.selected_nodes ||
Object.keys(canvas.selected_nodes).length <= 1
) {
fApplyMultiNode(node, newSelected)
} else {
for (const i in canvas.selected_nodes) {
fApplyMultiNode(canvas.selected_nodes[i], newSelected)
}
const nodes = canvas.selectedItems.size ? canvas.selectedItems : [node]
// Find top-left-most boundary
let offsetX = Infinity
let offsetY = Infinity
for (const item of nodes) {
if (item.pos == null)
throw new TypeError(
'Invalid node encountered on clone. `pos` was null.'
)
if (item.pos[0] < offsetX) offsetX = item.pos[0]
if (item.pos[1] < offsetY) offsetY = item.pos[1]
}
if (newSelected.size) {
canvas.selectNodes([...newSelected])
}
graph.afterChange()
canvas.setDirty(true, true)
canvas._deserializeItems(canvas._serializeItems(nodes), {
position: [offsetX + 5, offsetY + 5]
})
}
/**
@@ -2384,42 +2365,22 @@ export class LGraphCanvas
node &&
this.allow_interaction
) {
let newType = node.type
const items = this._deserializeItems(this._serializeItems([node]), {
position: node.pos
})
const cloned = items?.created[0] as LGraphNode | undefined
if (!cloned) return
if (node instanceof SubgraphNode) {
const cloned = node.subgraph.clone().asSerialisable()
cloned.pos[0] += 5
cloned.pos[1] += 5
const subgraph = graph.createSubgraph(cloned)
subgraph.configure(cloned)
newType = subgraph.id
}
const node_data = node.clone()?.serialize()
if (node_data?.type != null) {
// Ensure the cloned node is configured against the correct type (especially for SubgraphNodes)
node_data.type = newType
const cloned = LiteGraph.createNode(newType)
if (cloned) {
cloned.configure(node_data)
cloned.pos[0] += 5
cloned.pos[1] += 5
if (this.allow_dragnodes) {
pointer.onDragStart = (pointer) => {
graph.add(cloned, false)
this.#startDraggingItems(cloned, pointer)
}
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
} else {
// TODO: Check if before/after change are necessary here.
graph.beforeChange()
graph.add(cloned, false)
graph.afterChange()
}
return
if (this.allow_dragnodes) {
pointer.onDragStart = (pointer) => {
this.#startDraggingItems(cloned, pointer)
}
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
return
}
// Node clicked
@@ -3963,17 +3924,26 @@ export class LGraphCanvas
const { created, nodes, links, reroutes } = results
// const failedNodes: ISerialisedNode[] = []
const subgraphIdMap: Record<string, string> = {}
// SubgraphV2: Remove always-clone behaviour
//Update subgraph ids
for (const subgraphInfo of parsed.subgraphs)
subgraphInfo.id = subgraphIdMap[subgraphInfo.id] = createUuidv4()
const allNodeInfo: ISerialisedNode[] = [
parsed.nodes ? [parsed.nodes] : [],
parsed.subgraphs ? parsed.subgraphs.map((s) => s.nodes ?? []) : []
].flat(2)
for (const nodeInfo of allNodeInfo)
if (nodeInfo.type in subgraphIdMap)
nodeInfo.type = subgraphIdMap[nodeInfo.type]
// Subgraphs
for (const info of parsed.subgraphs) {
// SubgraphV2: Remove always-clone behaviour
const originalId = info.id
info.id = createUuidv4()
const subgraph = graph.createSubgraph(info)
subgraph.configure(info)
results.subgraphs.set(originalId, subgraph)
results.subgraphs.set(info.id, subgraph)
}
for (const info of parsed.subgraphs)
results.subgraphs.get(info.id)?.configure(info)
// Groups
for (const info of parsed.groups) {
@@ -3985,17 +3955,6 @@ export class LGraphCanvas
created.push(group)
}
// Update subgraph ids with nesting
function updateSubgraphIds(nodes: { type: string }[]) {
for (const info of nodes) {
const subgraph = results.subgraphs.get(info.type)
if (!subgraph) continue
info.type = subgraph.id
updateSubgraphIds(subgraph.nodes)
}
}
updateSubgraphIds(parsed.nodes)
// Nodes
for (const info of parsed.nodes) {
const node = info.type == null ? null : LiteGraph.createNode(info.type)

View File

@@ -618,4 +618,17 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
// Call parent serialize method
return super.serialize()
}
override clone() {
const clone = super.clone()
// force reasign so domWidgets reset ownership
// eslint-disable-next-line no-self-assign
this.properties.proxyWidgets = this.properties.proxyWidgets
//TODO: Consider deep cloning subgraphs here.
//It's the safest place to prevent creation of linked subgraphs
//But the frequency of clone().serialize() calls is likely to result in
//pollution of rootGraph.subgraphs
return clone
}
}