diff --git a/src/composables/canvas/useSelectionToolboxPosition.ts b/src/composables/canvas/useSelectionToolboxPosition.ts index 8e327b05f..efb8d5ca7 100644 --- a/src/composables/canvas/useSelectionToolboxPosition.ts +++ b/src/composables/canvas/useSelectionToolboxPosition.ts @@ -3,8 +3,12 @@ import type { Ref } from 'vue' import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync' import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems' -import { createBounds } from '@/lib/litegraph/src/litegraph' +import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags' +import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces' +import { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { useCanvasStore } from '@/stores/graphStore' +import { computeUnionBounds } from '@/utils/mathUtil' /** * Manages the position of the selection toolbox independently. @@ -16,6 +20,7 @@ export function useSelectionToolboxPosition( const canvasStore = useCanvasStore() const lgCanvas = canvasStore.getCanvas() const { getSelectableItems } = useSelectedLiteGraphItems() + const { shouldRenderVueNodes } = useVueFeatureFlags() // World position of selection center const worldPosition = ref({ x: 0, y: 0 }) @@ -34,17 +39,40 @@ export function useSelectionToolboxPosition( } visible.value = true - const bounds = createBounds(selectableItems) - if (!bounds) { - return + // Get bounds for all selected items + const allBounds: ReadOnlyRect[] = [] + for (const item of selectableItems) { + // Skip items without valid IDs + if (item.id == null) continue + + if (shouldRenderVueNodes.value && typeof item.id === 'string') { + // Use layout store for Vue nodes (only works with string IDs) + const layout = layoutStore.getNodeLayoutRef(item.id).value + if (layout) { + allBounds.push([ + layout.bounds.x, + layout.bounds.y, + layout.bounds.width, + layout.bounds.height + ]) + } + } else { + // Fallback to LiteGraph bounds for regular nodes or non-string IDs + if (item instanceof LGraphNode) { + const bounds = item.getBounding() + allBounds.push([bounds[0], bounds[1], bounds[2], bounds[3]] as const) + } + } } - const [xBase, y, width] = bounds + // Compute union bounds + const unionBounds = computeUnionBounds(allBounds) + if (!unionBounds) return worldPosition.value = { - x: xBase + width / 2, - y: y + x: unionBounds.x + unionBounds.width / 2, + y: unionBounds.y - 10 } updateTransform() diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index 10cb420eb..ca5d67120 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -19,6 +19,7 @@ import type { LayoutOperation, MoveNodeOperation, MoveRerouteOperation, + NodeBoundsUpdate, ResizeNodeOperation, SetNodeZIndexOperation } from '@/renderer/core/layout/types' @@ -1425,6 +1426,31 @@ class LayoutStoreImpl implements LayoutStore { getStateAsUpdate(): Uint8Array { return Y.encodeStateAsUpdate(this.ydoc) } + + /** + * Batch update node bounds using Yjs transaction for atomicity. + */ + batchUpdateNodeBounds(updates: NodeBoundsUpdate[]): void { + if (updates.length === 0) return + + // Set source to Vue for these DOM-driven updates + const originalSource = this.currentSource + this.currentSource = LayoutSource.Vue + + this.ydoc.transact(() => { + for (const { nodeId, bounds } of updates) { + const ynode = this.ynodes.get(nodeId) + if (!ynode) continue + + this.spatialIndex.update(nodeId, bounds) + ynode.set('bounds', bounds) + ynode.set('size', { width: bounds.width, height: bounds.height }) + } + }, this.currentActor) + + // Restore original source + this.currentSource = originalSource + } } // Create singleton instance diff --git a/src/renderer/core/layout/types.ts b/src/renderer/core/layout/types.ts index fdfcff430..1dbd936d9 100644 --- a/src/renderer/core/layout/types.ts +++ b/src/renderer/core/layout/types.ts @@ -31,6 +31,11 @@ export interface Bounds { height: number } +export interface NodeBoundsUpdate { + nodeId: NodeId + bounds: Bounds +} + export type NodeId = string export type LinkId = number export type RerouteId = number @@ -320,4 +325,9 @@ export interface LayoutStore { setActor(actor: string): void getCurrentSource(): LayoutSource getCurrentActor(): string + + // Batch updates + batchUpdateNodeBounds( + updates: Array<{ nodeId: NodeId; bounds: Bounds }> + ): void } diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index a880b34d0..b01300fd3 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -113,7 +113,6 @@