Move Frame Vue Nodes (#5886)

This pull request improves the selection and movement logic for groups
and nodes on the LiteGraph canvas, especially when using Vue-based node
rendering. The most notable changes are the addition of proper bounding
box handling for groups and a new coordinated movement mechanism that
updates both LiteGraph internals and the Vue layout store when dragging
nodes and groups.

**Selection and bounding box calculation:**

* Added support for including `LGraphGroup` bounding rectangles when
calculating the selection toolbox position, so groups are now properly
considered in selection overlays.
[[1]](diffhunk://#diff-57a51ac5e656e64ae7fd276d71b115058631621755de33b1eb8e8a4731d48713L8-R8)
[[2]](diffhunk://#diff-57a51ac5e656e64ae7fd276d71b115058631621755de33b1eb8e8a4731d48713R95-R97)

**Node and group movement synchronization (Vue nodes mode):**

* Introduced a new movement logic in `LGraphCanvas` for Vue nodes mode:
when dragging, groups and their child nodes are moved together, and all
affected node positions are batch-updated in both LiteGraph and the Vue
layout store via `moveNode`. This ensures canvas and UI stay in sync.
* Added imports for layout mutation operations and types to support the
above synchronization.

These changes make group selection and movement more robust and ensure
that UI and internal state remain consistent when using the Vue-based
node system.



https://github.com/user-attachments/assets/153792dc-08f2-4b53-b2bf-b0591ee76559

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5886-Move-Frame-Vue-Nodes-2806d73d365081e48b5ef96d6c6b6d6b)
by [Unito](https://www.unito.io)
This commit is contained in:
Johnpaul Chiwetelu
2025-10-03 03:05:33 +01:00
committed by GitHub
parent 4b1c165d43
commit 2970692176
3 changed files with 131 additions and 4 deletions

View File

@@ -5,7 +5,7 @@ import type { Ref } from 'vue'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
@@ -89,7 +89,7 @@ export function useSelectionToolboxPosition(
}
} else {
// Fallback to LiteGraph bounds for regular nodes or non-string IDs
if (item instanceof LGraphNode) {
if (item instanceof LGraphNode || item instanceof LGraphGroup) {
const bounds = item.getBounding()
allBounds.push([bounds[0], bounds[1], bounds[2], bounds[3]] as const)
}

View File

@@ -6,7 +6,9 @@ import {
LitegraphLinkAdapter
} from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import { CanvasPointer } from './CanvasPointer'
import type { ContextMenu } from './ContextMenu'
@@ -3431,8 +3433,13 @@ export class LGraphCanvas
const deltaX = delta[0] / this.ds.scale
const deltaY = delta[1] / this.ds.scale
for (const item of allItems) {
item.move(deltaX, deltaY, true)
if (LiteGraph.vueNodesMode) {
this.moveChildNodesInGroupVueMode(allItems, deltaX, deltaY)
} else {
for (const item of allItems) {
item.move(deltaX, deltaY, true)
}
}
this.#dirty()
@@ -8452,4 +8459,120 @@ export class LGraphCanvas
const setDirty = () => this.setDirty(true, true)
this.ds.animateToBounds(bounds, setDirty, options)
}
/**
* Calculate new position with delta
*/
private calculateNewPosition(
node: LGraphNode,
deltaX: number,
deltaY: number
): { x: number; y: number } {
return {
x: node.pos[0] + deltaX,
y: node.pos[1] + deltaY
}
}
/**
* Apply batched node position updates
*/
private applyNodePositionUpdates(
nodesToMove: Array<{ node: LGraphNode; newPos: { x: number; y: number } }>,
mutations: ReturnType<typeof useLayoutMutations>
): void {
for (const { node, newPos } of nodesToMove) {
// Update LiteGraph position first so next drag uses correct base position
node.pos[0] = newPos.x
node.pos[1] = newPos.y
// Then update layout store which will update Vue nodes
mutations.moveNode(node.id, newPos)
}
}
/**
* Initialize layout mutations with Canvas source
*/
private initLayoutMutations(): ReturnType<typeof useLayoutMutations> {
const mutations = useLayoutMutations()
mutations.setSource(LayoutSource.Canvas)
return mutations
}
/**
* Collect all nodes that are children of groups in the selection
*/
private collectNodesInGroups(items: Set<Positionable>): Set<LGraphNode> {
const nodesInGroups = new Set<LGraphNode>()
for (const item of items) {
if (item instanceof LGraphGroup) {
for (const child of item._children) {
if (child instanceof LGraphNode) {
nodesInGroups.add(child)
}
}
}
}
return nodesInGroups
}
/**
* Move group children (both nodes and non-nodes)
*/
private moveGroupChildren(
group: LGraphGroup,
deltaX: number,
deltaY: number,
nodesToMove: Array<{ node: LGraphNode; newPos: { x: number; y: number } }>
): void {
for (const child of group._children) {
if (child instanceof LGraphNode) {
const node = child as LGraphNode
nodesToMove.push({
node,
newPos: this.calculateNewPosition(node, deltaX, deltaY)
})
} else {
// Non-node children (nested groups, reroutes)
child.move(deltaX, deltaY)
}
}
}
moveChildNodesInGroupVueMode(
allItems: Set<Positionable>,
deltaX: number,
deltaY: number
) {
const mutations = this.initLayoutMutations()
const nodesInMovingGroups = this.collectNodesInGroups(allItems)
const nodesToMove: Array<{
node: LGraphNode
newPos: { x: number; y: number }
}> = []
// First, collect all the moves we need to make
for (const item of allItems) {
const isNode = item instanceof LGraphNode
if (isNode) {
const node = item as LGraphNode
if (nodesInMovingGroups.has(node)) {
continue
}
nodesToMove.push({
node,
newPos: this.calculateNewPosition(node, deltaX, deltaY)
})
} else if (item instanceof LGraphGroup) {
item.move(deltaX, deltaY, true)
this.moveGroupChildren(item, deltaX, deltaY, nodesToMove)
} else {
// Other items (reroutes, etc.)
item.move(deltaX, deltaY, true)
}
}
// Now apply all the node moves at once
this.applyNodePositionUpdates(nodesToMove, mutations)
}
}

View File

@@ -106,6 +106,10 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
return this._bounding
}
getBounding() {
return this._bounding
}
get nodes() {
return this._nodes
}