From b72e22f6bec717960395fcfa9923721b625afb6e Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 10 Sep 2025 22:38:49 -0700 Subject: [PATCH] Add Centralized Vue Node Size/Pos Tracking (#5442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add dom element resize observer registry for vue node components * Update src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts Co-authored-by: AustinMroz * refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeObserver sizing, centralized slot tracking, and small readability updates * chore: make TransformState interface non-exported to satisfy knip pre-push * Revert "chore: make TransformState interface non-exported to satisfy knip pre-push" This reverts commit 110ecf31dac3087683d3d53e908dbef3707efa31. * Revert "refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeObserver sizing, centralized slot tracking, and small readability updates" This reverts commit 428752619c2761a6cf10450124a634be7eae667e. * [refactor] Improve resize tracking composable documentation and test utilities - Rename parameters in useVueElementTracking for clarity (appIdentifier, trackingType) - Add comprehensive docstring with examples to prevent DOM attribute confusion - Extract mountLGraphNode test utility to eliminate repetitive mock setup - Add technical implementation notes documenting optimization decisions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * remove typo comment * convert to functional bounds collection * remove inline import * add interfaces for bounds mutations * remove change log * fix bounds collection when vue nodes turned off * fix title offset on y * move from resize observer to selection toolbox bounds --------- Co-authored-by: AustinMroz Co-authored-by: Benjamin Lu Co-authored-by: Claude --- .../canvas/useSelectionToolboxPosition.ts | 42 ++++- src/renderer/core/layout/store/layoutStore.ts | 26 +++ src/renderer/core/layout/types.ts | 10 ++ .../vueNodes/components/LGraphNode.vue | 4 +- .../composables/useVueNodeResizeTracking.ts | 155 ++++++++++++++++++ src/utils/mathUtil.ts | 50 +++++- .../vueNodes/components/LGraphNode.spec.ts | 116 +++++++++++++ tests-ui/tests/utils/mathUtil.test.ts | 97 +++++++++++ 8 files changed, 491 insertions(+), 9 deletions(-) create mode 100644 src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts create mode 100644 tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts create mode 100644 tests-ui/tests/utils/mathUtil.test.ts 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 @@