From 428752619c2761a6cf10450124a634be7eae667e Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 9 Sep 2025 16:20:25 -0700 Subject: [PATCH] refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeObserver sizing, centralized slot tracking, and small readability updates --- src/renderer/core/layout/TransformPane.vue | 3 +- src/renderer/core/layout/injectionKeys.ts | 18 ++ .../layout/slots/useDomSlotRegistration.ts | 229 ------------------ src/renderer/core/layout/store/layoutStore.ts | 33 ++- .../vueNodes/components/InputSlot.vue | 18 +- .../vueNodes/components/OutputSlot.vue | 18 +- .../composables/useSlotElementTracking.ts | 199 +++++++++++++++ .../composables/useVueNodeResizeTracking.ts | 102 +++++--- .../vueNodes/layout/useNodeLayout.ts | 8 +- 9 files changed, 335 insertions(+), 293 deletions(-) create mode 100644 src/renderer/core/layout/injectionKeys.ts delete mode 100644 src/renderer/core/layout/slots/useDomSlotRegistration.ts create mode 100644 src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts diff --git a/src/renderer/core/layout/TransformPane.vue b/src/renderer/core/layout/TransformPane.vue index 2f623257c..34e22c244 100644 --- a/src/renderer/core/layout/TransformPane.vue +++ b/src/renderer/core/layout/TransformPane.vue @@ -16,6 +16,7 @@ import { computed, provide } from 'vue' import { useCanvasTransformSync } from '@/composables/graph/useCanvasTransformSync' import { useTransformSettling } from '@/composables/graph/useTransformSettling' import { LGraphCanvas } from '@/lib/litegraph/src/litegraph' +import { TransformStateKey } from '@/renderer/core/layout/injectionKeys' import { useTransformState } from '@/renderer/core/layout/useTransformState' interface TransformPaneProps { @@ -39,7 +40,7 @@ const { isTransforming: isInteracting } = useTransformSettling(canvasElement, { trackPan: true }) -provide('transformState', { +provide(TransformStateKey, { camera, canvasToScreen, screenToCanvas, diff --git a/src/renderer/core/layout/injectionKeys.ts b/src/renderer/core/layout/injectionKeys.ts new file mode 100644 index 000000000..48306b063 --- /dev/null +++ b/src/renderer/core/layout/injectionKeys.ts @@ -0,0 +1,18 @@ +import type { InjectionKey } from 'vue' + +import type { Point } from '@/renderer/core/layout/types' + +export interface TransformState { + screenToCanvas: (p: Point) => Point + canvasToScreen: (p: Point) => Point + camera?: { x: number; y: number; z: number } + isNodeInViewport?: ( + nodePos: ArrayLike, + nodeSize: ArrayLike, + viewport: { width: number; height: number }, + margin?: number + ) => boolean +} + +export const TransformStateKey: InjectionKey = + Symbol('transformState') diff --git a/src/renderer/core/layout/slots/useDomSlotRegistration.ts b/src/renderer/core/layout/slots/useDomSlotRegistration.ts deleted file mode 100644 index 94a1f09e5..000000000 --- a/src/renderer/core/layout/slots/useDomSlotRegistration.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * DOM-based slot registration with performance optimization - * - * Measures the actual DOM position of a Vue slot connector and registers it - * into the LayoutStore so hit-testing and link rendering use the true position. - * - * Performance strategy: - * - Cache slot offset relative to node (avoids DOM reads during drag) - * - No measurements during pan/zoom (camera transforms don't change canvas coords) - * - Batch DOM reads via requestAnimationFrame - * - Only remeasure on structural changes (resize, collapse, LOD) - */ -import { - type Ref, - type WatchStopHandle, - nextTick, - onMounted, - onUnmounted, - ref, - watch -} from 'vue' - -import { LiteGraph } from '@/lib/litegraph/src/litegraph' -import { layoutStore } from '@/renderer/core/layout/store/layoutStore' -import type { Point as LayoutPoint } from '@/renderer/core/layout/types' - -import { getSlotKey } from './slotIdentifier' - -export type TransformState = { - screenToCanvas: (p: LayoutPoint) => LayoutPoint -} - -// Shared RAF queue for batching measurements -const measureQueue = new Set<() => void>() -let rafId: number | null = null -// Track mounted components to prevent execution on unmounted ones -const mountedComponents = new WeakSet() - -function scheduleMeasurement(fn: () => void) { - measureQueue.add(fn) - if (rafId === null) { - rafId = requestAnimationFrame(() => { - rafId = null - const batch = Array.from(measureQueue) - measureQueue.clear() - batch.forEach((measure) => measure()) - }) - } -} - -const cleanupFunctions = new WeakMap< - Ref, - { - stopWatcher?: WatchStopHandle - handleResize?: () => void - } ->() - -interface SlotRegistrationOptions { - nodeId: string - slotIndex: number - isInput: boolean - element: Ref - transform?: TransformState -} - -export function useDomSlotRegistration(options: SlotRegistrationOptions) { - const { nodeId, slotIndex, isInput, element: elRef, transform } = options - - // Early return if no nodeId - if (!nodeId || nodeId === '') { - return { - remeasure: () => {} - } - } - const slotKey = getSlotKey(nodeId, slotIndex, isInput) - // Track if this component is mounted - const componentToken = {} - - // Cached offset from node position (avoids DOM reads during drag) - const cachedOffset = ref(null) - const lastMeasuredBounds = ref(null) - - // Measure DOM and cache offset (expensive, minimize calls) - const measureAndCacheOffset = () => { - // Skip if component was unmounted - if (!mountedComponents.has(componentToken)) return - - const el = elRef.value - if (!el || !transform?.screenToCanvas) return - - const rect = el.getBoundingClientRect() - - // Skip if bounds haven't changed significantly (within 0.5px) - if (lastMeasuredBounds.value) { - const prev = lastMeasuredBounds.value - if ( - Math.abs(rect.left - prev.left) < 0.5 && - Math.abs(rect.top - prev.top) < 0.5 && - Math.abs(rect.width - prev.width) < 0.5 && - Math.abs(rect.height - prev.height) < 0.5 - ) { - return // No significant change - skip update - } - } - - lastMeasuredBounds.value = rect - - // Center of the visual connector (dot) in screen coords - const centerScreen = { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 - } - const centerCanvas = transform.screenToCanvas(centerScreen) - - // Cache offset from node position for fast updates during drag - const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value - if (nodeLayout) { - cachedOffset.value = { - x: centerCanvas.x - nodeLayout.position.x, - y: centerCanvas.y - nodeLayout.position.y - } - } - - updateSlotPosition(centerCanvas) - } - - // Fast update using cached offset (no DOM read) - const updateFromCachedOffset = () => { - if (!cachedOffset.value) { - // No cached offset yet, need to measure - scheduleMeasurement(measureAndCacheOffset) - return - } - - const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value - if (!nodeLayout) { - return - } - - // Calculate absolute position from node position + cached offset - const centerCanvas = { - x: nodeLayout.position.x + cachedOffset.value.x, - y: nodeLayout.position.y + cachedOffset.value.y - } - - updateSlotPosition(centerCanvas) - } - - // Update slot position in layout store - const updateSlotPosition = (centerCanvas: LayoutPoint) => { - const size = LiteGraph.NODE_SLOT_HEIGHT - const half = size / 2 - - layoutStore.updateSlotLayout(slotKey, { - nodeId, - index: slotIndex, - type: isInput ? 'input' : 'output', - position: { x: centerCanvas.x, y: centerCanvas.y }, - bounds: { - x: centerCanvas.x - half, - y: centerCanvas.y - half, - width: size, - height: size - } - }) - } - - onMounted(async () => { - // Mark component as mounted - mountedComponents.add(componentToken) - - // Initial measure after mount - await nextTick() - measureAndCacheOffset() - - // Subscribe to node position changes for fast cached updates - const nodeRef = layoutStore.getNodeLayoutRef(nodeId) - - const stopWatcher = watch( - nodeRef, - (newLayout) => { - if (newLayout) { - // Node moved/resized - update using cached offset - updateFromCachedOffset() - } - }, - { immediate: false } - ) - - // Store cleanup functions without type assertions - const cleanup = cleanupFunctions.get(elRef) || {} - cleanup.stopWatcher = stopWatcher - - // Window resize - remeasure as viewport changed - const handleResize = () => { - scheduleMeasurement(measureAndCacheOffset) - } - window.addEventListener('resize', handleResize, { passive: true }) - cleanup.handleResize = handleResize - cleanupFunctions.set(elRef, cleanup) - }) - - onUnmounted(() => { - // Mark component as unmounted - mountedComponents.delete(componentToken) - - // Clean up watchers and listeners - const cleanup = cleanupFunctions.get(elRef) - if (cleanup) { - if (cleanup.stopWatcher) cleanup.stopWatcher() - if (cleanup.handleResize) { - window.removeEventListener('resize', cleanup.handleResize) - } - cleanupFunctions.delete(elRef) - } - - // Remove from layout store - layoutStore.deleteSlotLayout(slotKey) - - // Remove from measurement queue if pending - measureQueue.delete(measureAndCacheOffset) - }) - - return { - // Expose for forced remeasure on structural changes - remeasure: () => scheduleMeasurement(measureAndCacheOffset) - } -} diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index 385e09bcd..633712f15 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -456,6 +456,20 @@ class LayoutStoreImpl implements LayoutStore { const existing = this.slotLayouts.get(key) if (existing) { + // Short-circuit if nothing changed to avoid spatial index churn + if ( + existing.nodeId === layout.nodeId && + existing.index === layout.index && + existing.type === layout.type && + existing.position.x === layout.position.x && + existing.position.y === layout.position.y && + existing.bounds.x === layout.bounds.x && + existing.bounds.y === layout.bounds.y && + existing.bounds.width === layout.bounds.width && + existing.bounds.height === layout.bounds.height + ) { + return + } // Update spatial index this.slotSpatialIndex.update(key, layout.bounds) } else { @@ -1443,9 +1457,26 @@ class LayoutStoreImpl implements LayoutStore { const ynode = this.ynodes.get(nodeId) if (!ynode) continue + // Short-circuit when bounds are unchanged to avoid churn + const currentBounds = this.getNodeField(ynode, 'bounds') + const sameBounds = + currentBounds.x === bounds.x && + currentBounds.y === bounds.y && + currentBounds.width === bounds.width && + currentBounds.height === bounds.height + if (sameBounds) continue + this.spatialIndex.update(nodeId, bounds) ynode.set('bounds', bounds) - ynode.set('size', { width: bounds.width, height: bounds.height }) + + // Keep size in sync with bounds + const currentSize = this.getNodeField(ynode, 'size') + if ( + currentSize.width !== bounds.width || + currentSize.height !== bounds.height + ) { + ynode.set('size', { width: bounds.width, height: bounds.height }) + } } }, this.currentActor) diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue index 124597d6b..78d9ebe57 100644 --- a/src/renderer/extensions/vueNodes/components/InputSlot.vue +++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue @@ -32,7 +32,6 @@ import { type ComponentPublicInstance, computed, - inject, onErrorCaptured, ref, watchEffect @@ -42,10 +41,7 @@ import { useErrorHandling } from '@/composables/useErrorHandling' import { getSlotColor } from '@/constants/slotColors' import { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph' // DOM-based slot registration for arbitrary positioning -import { - type TransformState, - useDomSlotRegistration -} from '@/renderer/core/layout/slots/useDomSlotRegistration' +import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking' import SlotConnectionDot from './SlotConnectionDot.vue' @@ -75,11 +71,6 @@ onErrorCaptured((error) => { // Get slot color based on type const slotColor = computed(() => getSlotColor(props.slotData.type)) -const transformState = inject( - 'transformState', - undefined -) - const connectionDotRef = ref | null>(null) @@ -92,11 +83,10 @@ watchEffect(() => { slotElRef.value = el || null }) -useDomSlotRegistration({ +useSlotElementTracking({ nodeId: props.nodeId ?? '', - slotIndex: props.index, + index: props.index, isInput: true, - element: slotElRef, - transform: transformState + element: slotElRef }) diff --git a/src/renderer/extensions/vueNodes/components/OutputSlot.vue b/src/renderer/extensions/vueNodes/components/OutputSlot.vue index e83019aa9..cecdc72d6 100644 --- a/src/renderer/extensions/vueNodes/components/OutputSlot.vue +++ b/src/renderer/extensions/vueNodes/components/OutputSlot.vue @@ -33,7 +33,6 @@ import { type ComponentPublicInstance, computed, - inject, onErrorCaptured, ref, watchEffect @@ -43,10 +42,7 @@ import { useErrorHandling } from '@/composables/useErrorHandling' import { getSlotColor } from '@/constants/slotColors' import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph' // DOM-based slot registration for arbitrary positioning -import { - type TransformState, - useDomSlotRegistration -} from '@/renderer/core/layout/slots/useDomSlotRegistration' +import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking' import SlotConnectionDot from './SlotConnectionDot.vue' @@ -77,11 +73,6 @@ onErrorCaptured((error) => { // Get slot color based on type const slotColor = computed(() => getSlotColor(props.slotData.type)) -const transformState = inject( - 'transformState', - undefined -) - const connectionDotRef = ref | null>(null) @@ -94,11 +85,10 @@ watchEffect(() => { slotElRef.value = el || null }) -useDomSlotRegistration({ +useSlotElementTracking({ nodeId: props.nodeId ?? '', - slotIndex: props.index, + index: props.index, isInput: false, - element: slotElRef, - transform: transformState + element: slotElRef }) diff --git a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts new file mode 100644 index 000000000..7367604c7 --- /dev/null +++ b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts @@ -0,0 +1,199 @@ +/** + * Centralized Slot Element Tracking + * + * Registers slot connector DOM elements per node, measures their canvas-space + * positions in a single batched pass, and caches offsets so that node moves + * update slot positions without DOM reads. + */ +import { type Ref, inject, nextTick, onMounted, onUnmounted, watch } from 'vue' + +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { TransformStateKey } from '@/renderer/core/layout/injectionKeys' +import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { Point } from '@/renderer/core/layout/types' + +type SlotEntry = { + el: HTMLElement + index: number + isInput: boolean + cachedOffset?: { x: number; y: number } +} + +type NodeEntry = { + nodeId: string + screenToCanvas?: (p: Point) => Point + slots: Map + stopWatch?: () => void +} + +// Registry of nodes and their slots +const nodeRegistry = new Map() + +// RAF batching +const pendingNodes = new Set() +let rafId: number | null = null + +function scheduleNodeMeasure(nodeId: string) { + pendingNodes.add(nodeId) + if (rafId == null) { + rafId = requestAnimationFrame(() => { + rafId = null + runBatchedMeasure() + }) + } +} + +function runBatchedMeasure() { + if (pendingNodes.size === 0) return + + // Read container origin once + const container = document.getElementById('graph-canvas-container') + const originRect = container?.getBoundingClientRect() + const originLeft = originRect?.left ?? 0 + const originTop = originRect?.top ?? 0 + + for (const nodeId of Array.from(pendingNodes)) { + pendingNodes.delete(nodeId) + const node = nodeRegistry.get(nodeId) + if (!node) continue + if (!node.screenToCanvas) continue + const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value + if (!nodeLayout) continue + + for (const [slotKey, entry] of node.slots) { + const rect = entry.el.getBoundingClientRect() + const centerScreen = { + x: rect.left + rect.width / 2 - originLeft, + y: rect.top + rect.height / 2 - originTop + } + const centerCanvas = node.screenToCanvas(centerScreen) + + // Cache offset relative to node position for fast updates later + entry.cachedOffset = { + x: centerCanvas.x - nodeLayout.position.x, + y: centerCanvas.y - nodeLayout.position.y + } + + // Persist layout in canvas coordinates + const size = LiteGraph.NODE_SLOT_HEIGHT + const half = size / 2 + layoutStore.updateSlotLayout(slotKey, { + nodeId, + index: entry.index, + type: entry.isInput ? 'input' : 'output', + position: { x: centerCanvas.x, y: centerCanvas.y }, + bounds: { + x: centerCanvas.x - half, + y: centerCanvas.y - half, + width: size, + height: size + } + }) + } + } +} + +function updateNodeSlotsFromCache(nodeId: string) { + const node = nodeRegistry.get(nodeId) + if (!node) return + const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value + if (!nodeLayout) return + + for (const [slotKey, entry] of node.slots) { + if (!entry.cachedOffset) { + // schedule a remeasure to seed offset + scheduleNodeMeasure(nodeId) + continue + } + const centerCanvas = { + x: nodeLayout.position.x + entry.cachedOffset.x, + y: nodeLayout.position.y + entry.cachedOffset.y + } + const size = LiteGraph.NODE_SLOT_HEIGHT + const half = size / 2 + layoutStore.updateSlotLayout(slotKey, { + nodeId, + index: entry.index, + type: entry.isInput ? 'input' : 'output', + position: { x: centerCanvas.x, y: centerCanvas.y }, + bounds: { + x: centerCanvas.x - half, + y: centerCanvas.y - half, + width: size, + height: size + } + }) + } +} + +export function useSlotElementTracking(options: { + nodeId: string + index: number + isInput: boolean + element: Ref +}) { + const { nodeId, index, isInput, element } = options + + // Get transform utilities from TransformPane + const transformState = inject(TransformStateKey) + + onMounted(async () => { + if (!nodeId) return + await nextTick() + const el = element.value + if (!el) return + + // Ensure node entry + let node = nodeRegistry.get(nodeId) + if (!node) { + node = { + nodeId, + screenToCanvas: transformState?.screenToCanvas, + slots: new Map() + } + nodeRegistry.set(nodeId, node) + // Subscribe once per node to layout changes for fast cached updates + const nodeRef = layoutStore.getNodeLayoutRef(nodeId) + const stop = watch( + nodeRef, + (newLayout, oldLayout) => { + if (newLayout && oldLayout) { + // Update from cache on any position/size change + updateNodeSlotsFromCache(nodeId) + } + }, + { flush: 'post' } + ) + node.stopWatch = () => stop() + } + + // Register slot + const slotKey = getSlotKey(nodeId, index, isInput) + node.slots.set(slotKey, { el, index, isInput }) + + // Seed measurement + scheduleNodeMeasure(nodeId) + }) + + onUnmounted(() => { + if (!nodeId) return + const node = nodeRegistry.get(nodeId) + if (!node) return + + // Remove this slot from registry and layout + const slotKey = getSlotKey(nodeId, index, isInput) + node.slots.delete(slotKey) + layoutStore.deleteSlotLayout(slotKey) + + // If node has no more slots, clean up + if (node.slots.size === 0) { + if (node.stopWatch) node.stopWatch() + nodeRegistry.delete(nodeId) + } + }) + + return { + remeasure: () => scheduleNodeMeasure(nodeId) + } +} diff --git a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts index 843cf5de1..c4b51c57f 100644 --- a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts @@ -8,11 +8,20 @@ * Supports different element types (nodes, slots, widgets, etc.) with * customizable data attributes and update handlers. */ -import { getCurrentInstance, onMounted, onUnmounted } from 'vue' +import { getCurrentInstance, inject, onMounted, onUnmounted } from 'vue' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { TransformStateKey } from '@/renderer/core/layout/injectionKeys' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { Point } from '@/renderer/core/layout/types' import type { Bounds, NodeId } from '@/renderer/core/layout/types' +// Per-element conversion context +const elementConversion = new WeakMap< + HTMLElement, + { screenToCanvas?: (p: Point) => Point } +>() + /** * Configuration for different types of tracked elements */ @@ -44,14 +53,20 @@ const trackingConfigs: Map = new Map([ // Single ResizeObserver instance for all Vue elements const resizeObserver = new ResizeObserver((entries) => { - // Group updates by element type + // Group updates by type, then flush via each config's handler const updatesByType = new Map>() + // Read container origin once per batch to avoid repeated layout reads + const container = document.getElementById('graph-canvas-container') + const originRect = container?.getBoundingClientRect() + const originLeft = originRect?.left ?? 0 + const originTop = originRect?.top ?? 0 + for (const entry of entries) { if (!(entry.target instanceof HTMLElement)) continue const element = entry.target - // Find which type this element belongs to + // Identify type + id via config dataAttribute let elementType: string | undefined let elementId: string | undefined @@ -66,31 +81,54 @@ const resizeObserver = new ResizeObserver((entries) => { if (!elementType || !elementId) continue - const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0] + // Use contentBoxSize when available; fall back to contentRect for older engines/tests + const contentBox = Array.isArray(entry.contentBoxSize) + ? entry.contentBoxSize[0] + : { + inlineSize: entry.contentRect.width, + blockSize: entry.contentRect.height + } + const width = contentBox.inlineSize + const height = contentBox.blockSize + + // Screen-space rect const rect = element.getBoundingClientRect() + let bounds: Bounds = { x: rect.left, y: rect.top, width, height } - const bounds: Bounds = { - x: rect.left, - y: rect.top, - width, - height: height-LiteGraph.NODE_TITLE_HEIGHT + // Convert to canvas space and adjust for title band when possible + const ctx = elementConversion.get(element) + if (ctx?.screenToCanvas) { + const topLeftCanvas = ctx.screenToCanvas({ + x: bounds.x - originLeft, + y: bounds.y - originTop + }) + const dimCanvas = ctx.screenToCanvas({ + x: bounds.width, + y: bounds.height + }) + const originCanvas = ctx.screenToCanvas({ x: 0, y: 0 }) + const canvasWidth = Math.max(0, dimCanvas.x - originCanvas.x) + const canvasHeight = Math.max(0, dimCanvas.y - originCanvas.y) + bounds = { + x: topLeftCanvas.x, + y: topLeftCanvas.y + LiteGraph.NODE_TITLE_HEIGHT, + width: canvasWidth, + height: Math.max(0, canvasHeight - LiteGraph.NODE_TITLE_HEIGHT) + } } - if (!updatesByType.has(elementType)) { - updatesByType.set(elementType, []) - } - const updates = updatesByType.get(elementType) - if (updates) { - updates.push({ id: elementId, bounds }) + let updates = updatesByType.get(elementType) + if (!updates) { + updates = [] + updatesByType.set(elementType, updates) } + updates.push({ id: elementId, bounds }) } - // Process updates by type + // Flush per-type for (const [type, updates] of updatesByType) { const config = trackingConfigs.get(type) - if (config && updates.length > 0) { - config.updateHandler(updates) - } + if (config && updates.length) config.updateHandler(updates) } }) @@ -119,16 +157,23 @@ export function useVueElementTracking( appIdentifier: string, trackingType: string ) { + // For canvas-space conversion: provided by TransformPane + const transformState = inject(TransformStateKey) + onMounted(() => { const element = getCurrentInstance()?.proxy?.$el if (!(element instanceof HTMLElement) || !appIdentifier) return const config = trackingConfigs.get(trackingType) - if (config) { - // Set the appropriate data attribute - element.dataset[config.dataAttribute] = appIdentifier - resizeObserver.observe(element) + if (!config) return // Set the data attribute expected by the RO pipeline for this type + element.dataset[config.dataAttribute] = appIdentifier + // Remember transformer for this element + if (transformState?.screenToCanvas) { + elementConversion.set(element, { + screenToCanvas: transformState.screenToCanvas + }) } + resizeObserver.observe(element) }) onUnmounted(() => { @@ -136,10 +181,11 @@ export function useVueElementTracking( if (!(element instanceof HTMLElement)) return const config = trackingConfigs.get(trackingType) - if (config) { - // Remove the data attribute - delete element.dataset[config.dataAttribute] - resizeObserver.unobserve(element) - } + if (!config) return + + // Remove the data attribute and observer + delete element.dataset[config.dataAttribute] + resizeObserver.unobserve(element) + elementConversion.delete(element) }) } diff --git a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts index 407a14243..b70940470 100644 --- a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts +++ b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts @@ -6,6 +6,7 @@ */ import { computed, inject } from 'vue' +import { TransformStateKey } from '@/renderer/core/layout/injectionKeys' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { LayoutSource, type Point } from '@/renderer/core/layout/types' @@ -19,12 +20,7 @@ export function useNodeLayout(nodeId: string) { const mutations = useLayoutMutations() // Get transform utilities from TransformPane if available - const transformState = inject('transformState') as - | { - canvasToScreen: (point: Point) => Point - screenToCanvas: (point: Point) => Point - } - | undefined + const transformState = inject(TransformStateKey) // Get the customRef for this node (shared write access) const layoutRef = store.getNodeLayoutRef(nodeId)