From 2970692176a6c091773463ffb303238f33082679 Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Fri, 3 Oct 2025 03:05:33 +0100 Subject: [PATCH] Move Frame Vue Nodes (#5886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../canvas/useSelectionToolboxPosition.ts | 4 +- src/lib/litegraph/src/LGraphCanvas.ts | 127 +++++++++++++++++- src/lib/litegraph/src/LGraphGroup.ts | 4 + 3 files changed, 131 insertions(+), 4 deletions(-) diff --git a/src/composables/canvas/useSelectionToolboxPosition.ts b/src/composables/canvas/useSelectionToolboxPosition.ts index 4d725dd746..70be10bd3e 100644 --- a/src/composables/canvas/useSelectionToolboxPosition.ts +++ b/src/composables/canvas/useSelectionToolboxPosition.ts @@ -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) } diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 2ec731ff04..38a79dfac7 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -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 + ): 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 { + const mutations = useLayoutMutations() + mutations.setSource(LayoutSource.Canvas) + return mutations + } + + /** + * Collect all nodes that are children of groups in the selection + */ + private collectNodesInGroups(items: Set): Set { + const nodesInGroups = new Set() + 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, + 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) + } } diff --git a/src/lib/litegraph/src/LGraphGroup.ts b/src/lib/litegraph/src/LGraphGroup.ts index 5d922dcf5d..9e14cf277c 100644 --- a/src/lib/litegraph/src/LGraphGroup.ts +++ b/src/lib/litegraph/src/LGraphGroup.ts @@ -106,6 +106,10 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { return this._bounding } + getBounding() { + return this._bounding + } + get nodes() { return this._nodes }