π€ Filed by an LLM agent using DrJKL's account, not by DrJKL.
Summary
layoutStore.batchUpdateNodeBounds conditionally subtracts NODE_TITLE_HEIGHT from the bounds it is given, gated on LayoutSource β which is ambient global state that leaks. After any Vue-node ResizeObserver flush, subsequent move operations silently shrink the node by one title height per call, and report a size change that fires LGraphNode.onResize.
Mechanism
useVueNodeResizeTracking.ts:254 sets the source and never restores it:
if (updatesByType.size > 0) {
layoutStore.setSource(LayoutSource.DOM) // no restore
...
}
-
batchUpdateNodeBounds restores currentSource to originalSource (layoutStore.ts:1328) β i.e. back to DOM. So the source stays DOM until some other writer sets it.
-
While the source is DOM, batchUpdateNodeBounds rewrites the caller's height (layoutStore.ts:1294-1306):
const shouldNormalizeHeights = originalSource === LayoutSource.DOM
...
boundsRecord[nodeId] = shouldNormalizeHeights
? { ...bounds, height: removeNodeTitleHeight(bounds.height) }
: bounds
This is correct for exactly one caller β the ResizeObserver handler, which passes raw border-box height. The other callers (layoutMutations.batchMoveNodes, useNodeDrag snap-on-release, LGraphCanvas paste) pass an already-normalized store height, so the subtraction is pure corruption.
handleBatchUpdateBounds then sees rect[3] !== bounds.height and pushes to sizeChangedNodeIds (layoutStore.ts:1100-1106), so notifyLayoutChanges fires node.onResize(node.size) for what the user experienced as a drag.
The ResizeObserver is a single module-level instance observing every Vue node, so a resize on one node poisons a drag of another. useNodeDrag.ts:98 sets Vue at startDrag, which only protects the drag until the first mid-drag RO flush (preview image load, progress badge, widget hydration).
Impact
- Node loses
NODE_TITLE_HEIGHT per affected move.
- Spurious
onResize callbacks β visible to custom nodes, so extension-facing.
Suggested fix
Normalize at the call site. Have useVueNodeResizeTracking.ts:73 subtract the title height before calling, so batchUpdateNodeBounds takes its bounds argument literally and shouldNormalizeHeights / the originalSource read can be deleted. LayoutSource should describe provenance for listeners, not gate a data transform.
Related
Surfaced while reviewing #14133 (#14133 (comment)). Pre-existing β that PR does not touch batchUpdateNodeBounds, and the predecessor useLayoutSync fired onResize on the same corrupted write via its own size diff.
Summary
layoutStore.batchUpdateNodeBoundsconditionally subtractsNODE_TITLE_HEIGHTfrom the bounds it is given, gated onLayoutSourceβ which is ambient global state that leaks. After any Vue-nodeResizeObserverflush, subsequent move operations silently shrink the node by one title height per call, and report a size change that firesLGraphNode.onResize.Mechanism
useVueNodeResizeTracking.ts:254sets the source and never restores it:batchUpdateNodeBoundsrestorescurrentSourcetooriginalSource(layoutStore.ts:1328) β i.e. back toDOM. So the source staysDOMuntil some other writer sets it.While the source is
DOM,batchUpdateNodeBoundsrewrites the caller's height (layoutStore.ts:1294-1306):This is correct for exactly one caller β the ResizeObserver handler, which passes raw border-box height. The other callers (
layoutMutations.batchMoveNodes,useNodeDragsnap-on-release,LGraphCanvaspaste) pass an already-normalized store height, so the subtraction is pure corruption.handleBatchUpdateBoundsthen seesrect[3] !== bounds.heightand pushes tosizeChangedNodeIds(layoutStore.ts:1100-1106), sonotifyLayoutChangesfiresnode.onResize(node.size)for what the user experienced as a drag.The
ResizeObserveris a single module-level instance observing every Vue node, so a resize on one node poisons a drag of another.useNodeDrag.ts:98setsVueatstartDrag, which only protects the drag until the first mid-drag RO flush (preview image load, progress badge, widget hydration).Impact
NODE_TITLE_HEIGHTper affected move.onResizecallbacks β visible to custom nodes, so extension-facing.Suggested fix
Normalize at the call site. Have
useVueNodeResizeTracking.ts:73subtract the title height before calling, sobatchUpdateNodeBoundstakes itsboundsargument literally andshouldNormalizeHeights/ theoriginalSourceread can be deleted.LayoutSourceshould describe provenance for listeners, not gate a data transform.Related
Surfaced while reviewing #14133 (#14133 (comment)). Pre-existing β that PR does not touch
batchUpdateNodeBounds, and the predecessoruseLayoutSyncfiredonResizeon the same corrupted write via its own size diff.