Compare commits

...

5 Commits

Author SHA1 Message Date
Austin
168fa5295e Sync size once when first mounting node component
Open the gate a little
2026-04-30 21:18:07 -07:00
Austin
63eafe8f15 Fix skip of callback 2026-04-30 21:18:07 -07:00
Austin
8f91f1aada Fix test 2026-04-30 21:18:07 -07:00
Austin
b2c47928fb Simplify layout initialization 2026-04-30 21:18:06 -07:00
Austin
bcf7f1d2ad Remove resizeObsever, cleanup dead code 2026-04-30 21:18:06 -07:00
6 changed files with 37 additions and 711 deletions

View File

@@ -5,7 +5,6 @@
import { reactiveComputed } from '@vueuse/core'
import { reactive, shallowReactive } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
import { matchPromotedInput } from '@/core/graph/subgraph/matchPromotedInput'
@@ -581,58 +580,23 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
originalCallback?: (node: LGraphNode) => void
) => {
const id = String(node.id)
// Store non-reactive reference to original node
nodeRefs.set(id, node)
// Extract initial data for Vue (may be incomplete during graph configure)
vueNodeData.set(id, extractVueNodeData(node))
const initializeVueNodeLayout = () => {
// Check if the node was removed mid-sequence
if (!nodeRefs.has(id)) return
const existingLayout = layoutStore.getNodeLayoutRef(id).value
if (existingLayout) return originalCallback?.(node)
// Extract actual positions after configure() has potentially updated them
const nodePosition = { x: node.pos[0], y: node.pos[1] }
const nodeSize = { width: node.size[0], height: node.size[1] }
const nodePosition = { x: node.pos[0], y: node.pos[1] }
const nodeSize = { width: node.size[0], height: node.size[1] }
setSource(LayoutSource.Canvas)
void createNode(id, {
position: nodePosition,
size: nodeSize,
zIndex: node.order || 0,
visible: true
})
// Skip layout creation if it already exists
// (e.g. in-place node replacement where the old node's layout is reused for the new node with the same ID).
const existingLayout = layoutStore.getNodeLayoutRef(id).value
if (existingLayout) return
// Add node to layout store with final positions
setSource(LayoutSource.Canvas)
void createNode(id, {
position: nodePosition,
size: nodeSize,
zIndex: node.order || 0,
visible: true
})
}
// Check if we're in the middle of configuring the graph (workflow loading)
if (window.app?.configuringGraph) {
// During workflow loading - defer layout initialization until configure completes
// Chain our callback with any existing onAfterGraphConfigured callback
node.onAfterGraphConfigured = useChainCallback(
node.onAfterGraphConfigured,
() => {
// Re-extract data now that configure() has populated title/slots/widgets/etc.
vueNodeData.set(id, extractVueNodeData(node))
initializeVueNodeLayout()
}
)
} else {
// Not during workflow loading - initialize layout immediately
// This handles individual node additions during normal operation
initializeVueNodeLayout()
}
// Call original callback if provided
if (originalCallback) {
void originalCallback(node)
}
return originalCallback?.(node)
}
/**

View File

@@ -9,7 +9,6 @@ import { createI18n } from 'vue-i18n'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
@@ -49,13 +48,6 @@ vi.mock(
}
)
vi.mock(
'@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking',
() => ({
useVueElementTracking: vi.fn()
})
)
vi.mock('@/scripts/app', () => ({
app: {
rootGraph: { getNodeById: vi.fn() },
@@ -188,12 +180,6 @@ describe('LGraphNode', () => {
})
})
it('should call resize tracking composable with node ID', () => {
renderLGraphNode({ nodeData: mockNodeData })
expect(useVueElementTracking).toHaveBeenCalledWith('test-node-123', 'node')
})
it('should render with data-node-id attribute', () => {
const { container } = renderLGraphNode({ nodeData: mockNodeData })

View File

@@ -293,7 +293,6 @@ import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
import { usePartitionedBadges } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges'
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
@@ -343,8 +342,6 @@ const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
useNodeEventHandlers()
const { bringNodeToFront } = useNodeZIndex()
useVueElementTracking(String(nodeData.id), 'node')
const { selectedNodeIds, isGhostPlacing } = storeToRefs(useCanvasStore())
const isSelected = computed(() => {
return selectedNodeIds.value.has(nodeData.id)
@@ -464,6 +461,25 @@ function initSizeStyles() {
el.style.setProperty(`--node-height${suffix}`, `${fullHeight}px`)
}
function syncSizeToLayoutStore() {
//Write size back to layout store
//This happens prior to layout subscription so feedback loops can not occur
//Still dangerous
const el = nodeContainerRef.value
if (!el) return
const bounds = el.getBoundingClientRect()
const width = Math.max(bounds.width - 1, size.value.width)
const height = Math.max(
bounds.height - 1 - LiteGraph.NODE_TITLE_HEIGHT,
size.value.height
)
if (size.value.height !== height || size.value.width !== width) {
mutations.setSource(LayoutSource.Vue)
mutations.resizeNode(nodeData.id, { width, height })
}
}
/**
* Handle external size changes (e.g., from extensions calling node.setSize()).
* Updates CSS variables when layoutStore changes from Canvas/External source.
@@ -491,6 +507,8 @@ let unsubscribeLayoutChange: (() => void) | null = null
onMounted(() => {
initSizeStyles()
syncSizeToLayoutStore()
unsubscribeLayoutChange = layoutStore.onNodeChange(
nodeData.id,
handleLayoutChange
@@ -517,10 +535,9 @@ const { startResize } = useNodeResize((result, element) => {
element.style.setProperty('--node-height', `${result.size.height}px`)
// Update position for non-SE corner resizing
if (result.position) {
mutations.setSource(LayoutSource.Vue)
mutations.moveNode(nodeData.id, result.position)
}
mutations.setSource(LayoutSource.Vue)
mutations.resizeNode(nodeData.id, result.size)
if (result.position) mutations.moveNode(nodeData.id, result.position)
})
const handleResizePointerDown = (

View File

@@ -1,320 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { Ref } from 'vue'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { NodeId, NodeLayout } from '@/renderer/core/layout/types'
type ResizeEntryLike = Pick<
ResizeObserverEntry,
| 'target'
| 'borderBoxSize'
| 'contentBoxSize'
| 'devicePixelContentBoxSize'
| 'contentRect'
>
const resizeObserverState = vi.hoisted(() => {
const state = {
callback: null as ResizeObserverCallback | null,
observe: vi.fn<(element: Element) => void>(),
unobserve: vi.fn<(element: Element) => void>(),
disconnect: vi.fn<() => void>()
}
const MockResizeObserver: typeof ResizeObserver = class MockResizeObserver implements ResizeObserver {
observe = state.observe
unobserve = state.unobserve
disconnect = state.disconnect
constructor(callback: ResizeObserverCallback) {
state.callback = callback
}
}
globalThis.ResizeObserver = MockResizeObserver
return state
})
const testState = vi.hoisted(() => ({
linearMode: false,
nodeLayouts: new Map<NodeId, NodeLayout>(),
batchUpdateNodeBounds: vi.fn(),
setSource: vi.fn(),
syncNodeSlotLayoutsFromDOM: vi.fn()
}))
vi.mock('@vueuse/core', () => ({
useDocumentVisibility: () => ref<'visible' | 'hidden'>('visible'),
createSharedComposable: <T>(fn: T) => fn
}))
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
linearMode: testState.linearMode
})
}))
vi.mock('@/composables/element/useCanvasPositionConversion', () => ({
useSharedCanvasPositionConversion: () => ({
clientPosToCanvasPos: ([x, y]: [number, number]) => [x, y]
})
}))
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
layoutStore: {
batchUpdateNodeBounds: testState.batchUpdateNodeBounds,
setSource: testState.setSource,
getNodeLayoutRef: (nodeId: NodeId): Ref<NodeLayout | null> =>
ref<NodeLayout | null>(testState.nodeLayouts.get(nodeId) ?? null)
}
}))
vi.mock('./useSlotElementTracking', () => ({
syncNodeSlotLayoutsFromDOM: testState.syncNodeSlotLayoutsFromDOM
}))
import './useVueNodeResizeTracking'
function createResizeEntry(options?: {
nodeId?: NodeId
width?: number
height?: number
left?: number
top?: number
collapsed?: boolean
}) {
const {
nodeId = 'test-node',
width = 240,
height = 180,
left = 100,
top = 200,
collapsed = false
} = options ?? {}
const element = document.createElement('div')
element.dataset.nodeId = nodeId
if (collapsed) {
element.dataset.collapsed = ''
}
const rectSpy = vi.fn(() => new DOMRect(left, top, width, height))
element.getBoundingClientRect = rectSpy
const boxSizes = [{ inlineSize: width, blockSize: height }]
const entry = {
target: element,
borderBoxSize: boxSizes,
contentBoxSize: boxSizes,
devicePixelContentBoxSize: boxSizes,
contentRect: new DOMRect(left, top, width, height)
} satisfies ResizeEntryLike
return {
entry,
rectSpy
}
}
function createObserverMock(): ResizeObserver {
return {
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn()
}
}
function seedNodeLayout(options: {
nodeId: NodeId
left: number
top: number
width: number
height: number
}) {
const { nodeId, left, top, width, height } = options
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
const contentHeight = height - titleHeight
testState.nodeLayouts.set(nodeId, {
id: nodeId,
position: { x: left, y: top + titleHeight },
size: { width, height: contentHeight },
zIndex: 0,
visible: true,
bounds: {
x: left,
y: top + titleHeight,
width,
height: contentHeight
}
})
}
describe('useVueNodeResizeTracking', () => {
beforeEach(() => {
testState.linearMode = false
testState.nodeLayouts.clear()
testState.batchUpdateNodeBounds.mockReset()
testState.setSource.mockReset()
testState.syncNodeSlotLayoutsFromDOM.mockReset()
resizeObserverState.observe.mockReset()
resizeObserverState.unobserve.mockReset()
resizeObserverState.disconnect.mockReset()
})
it('skips repeated no-op resize entries after first measurement', () => {
const nodeId = 'test-node'
const width = 240
const height = 180
const left = 100
const top = 200
const { entry, rectSpy } = createResizeEntry({
nodeId,
width,
height,
left,
top
})
seedNodeLayout({ nodeId, left, top, width, height })
resizeObserverState.callback?.([entry], createObserverMock())
// When layout store already has correct position, getBoundingClientRect
// is not needed — position is read from the store instead.
expect(rectSpy).not.toHaveBeenCalled()
expect(testState.setSource).not.toHaveBeenCalled()
expect(testState.batchUpdateNodeBounds).not.toHaveBeenCalled()
expect(testState.syncNodeSlotLayoutsFromDOM).not.toHaveBeenCalled()
testState.setSource.mockReset()
testState.batchUpdateNodeBounds.mockReset()
testState.syncNodeSlotLayoutsFromDOM.mockReset()
resizeObserverState.callback?.([entry], createObserverMock())
expect(rectSpy).not.toHaveBeenCalled()
expect(testState.setSource).not.toHaveBeenCalled()
expect(testState.batchUpdateNodeBounds).not.toHaveBeenCalled()
expect(testState.syncNodeSlotLayoutsFromDOM).not.toHaveBeenCalled()
})
it('preserves layout store position when size matches but DOM position differs', () => {
const nodeId = 'test-node'
const width = 240
const height = 180
const { entry, rectSpy } = createResizeEntry({
nodeId,
width,
height,
left: 100,
top: 200
})
seedNodeLayout({
nodeId,
left: 90,
top: 190,
width,
height
})
resizeObserverState.callback?.([entry], createObserverMock())
// Position from DOM should NOT override layout store position
expect(rectSpy).not.toHaveBeenCalled()
expect(testState.setSource).not.toHaveBeenCalled()
expect(testState.batchUpdateNodeBounds).not.toHaveBeenCalled()
})
it('updates node bounds + slot layouts when size changes', () => {
const nodeId = 'test-node'
const { entry } = createResizeEntry({
nodeId,
width: 240,
height: 180,
left: 100,
top: 200
})
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
seedNodeLayout({
nodeId,
left: 100,
top: 200,
width: 220,
height: 140
})
resizeObserverState.callback?.([entry], createObserverMock())
expect(testState.setSource).toHaveBeenCalledWith(LayoutSource.DOM)
expect(testState.batchUpdateNodeBounds).toHaveBeenCalledWith([
{
nodeId,
bounds: {
x: 100,
y: 200 + titleHeight,
width: 240,
height: 180
}
}
])
expect(testState.syncNodeSlotLayoutsFromDOM).toHaveBeenCalledWith(nodeId)
})
it('writes collapsed dimensions through the normal bounds path', () => {
const nodeId = 'test-node'
const collapsedWidth = 200
const collapsedHeight = 40
const { entry } = createResizeEntry({
nodeId,
width: collapsedWidth,
height: collapsedHeight,
left: 100,
top: 200,
collapsed: true
})
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
// Seed with larger expanded size so the collapsed write is a real change
seedNodeLayout({ nodeId, left: 100, top: 200, width: 240, height: 180 })
resizeObserverState.callback?.([entry], createObserverMock())
expect(testState.setSource).toHaveBeenCalledWith(LayoutSource.DOM)
expect(testState.batchUpdateNodeBounds).toHaveBeenCalledWith([
{
nodeId,
bounds: {
x: 100,
y: 200 + titleHeight,
width: collapsedWidth,
height: collapsedHeight
}
}
])
expect(testState.syncNodeSlotLayoutsFromDOM).toHaveBeenCalledWith(nodeId)
})
it('updates bounds with expanded dimensions on collapse-to-expand transition', () => {
const nodeId = 'test-node'
// Seed with smaller (collapsed) size so expand triggers a real bounds update
seedNodeLayout({ nodeId, left: 100, top: 200, width: 200, height: 10 })
const { entry } = createResizeEntry({
nodeId,
width: 240,
height: 180,
left: 100,
top: 200
})
resizeObserverState.callback?.([entry], createObserverMock())
expect(testState.setSource).toHaveBeenCalledWith(LayoutSource.DOM)
expect(testState.batchUpdateNodeBounds).toHaveBeenCalled()
})
})

View File

@@ -1,305 +0,0 @@
/**
* Generic Vue Element Tracking System
*
* Automatically tracks DOM size and position changes for Vue-rendered elements
* and syncs them to the layout store. Uses a single shared ResizeObserver for
* performance, with elements identified by configurable data attributes.
*
* Supports different element types (nodes, slots, widgets, etc.) with
* customizable data attributes and update handlers.
*/
import { getCurrentInstance, onMounted, onUnmounted, watch } from 'vue'
import { useDocumentVisibility } from '@vueuse/core'
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { Bounds, NodeId } from '@/renderer/core/layout/types'
import { LayoutSource } from '@/renderer/core/layout/types'
import {
isBoundsEqual,
isSizeEqual
} from '@/renderer/core/layout/utils/geometry'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { syncNodeSlotLayoutsFromDOM } from './useSlotElementTracking'
/**
* Generic update item for element bounds tracking
*/
interface ElementBoundsUpdate {
/** Element identifier (could be nodeId, widgetId, slotId, etc.) */
id: string
/** Updated bounds */
bounds: Bounds
}
interface CachedNodeMeasurement {
nodeId: NodeId
bounds: Bounds
}
/**
* Configuration for different types of tracked elements
*/
interface ElementTrackingConfig {
/** Data attribute name (e.g., 'nodeId') */
dataAttribute: string
/** Handler for processing bounds updates */
updateHandler: (updates: ElementBoundsUpdate[]) => void
}
/**
* Registry of tracking configurations by element type
*/
const trackingConfigs: Map<string, ElementTrackingConfig> = new Map([
[
'node',
{
dataAttribute: 'nodeId',
updateHandler: (updates) => {
const nodeUpdates = updates.map(({ id, bounds }) => ({
nodeId: id as NodeId,
bounds
}))
layoutStore.batchUpdateNodeBounds(nodeUpdates)
}
}
]
])
// Elements whose ResizeObserver fired while the tab was hidden
const deferredElements = new Set<HTMLElement>()
const elementsNeedingFreshMeasurement = new WeakSet<HTMLElement>()
const cachedNodeMeasurements = new WeakMap<HTMLElement, CachedNodeMeasurement>()
const visibility = useDocumentVisibility()
function markElementForFreshMeasurement(element: HTMLElement) {
elementsNeedingFreshMeasurement.add(element)
cachedNodeMeasurements.delete(element)
}
watch(visibility, (state) => {
if (state !== 'visible' || deferredElements.size === 0) return
// Re-observe deferred elements to trigger fresh measurements
for (const element of deferredElements) {
if (element.isConnected) {
markElementForFreshMeasurement(element)
resizeObserver.observe(element)
}
}
deferredElements.clear()
})
// Single ResizeObserver instance for all Vue elements
const resizeObserver = new ResizeObserver((entries) => {
if (useCanvasStore().linearMode) return
// Skip measurements when tab is hidden — bounding rects are unreliable
if (visibility.value === 'hidden') {
for (const entry of entries) {
if (entry.target instanceof HTMLElement) {
deferredElements.add(entry.target)
markElementForFreshMeasurement(entry.target)
resizeObserver.unobserve(entry.target)
}
}
return
}
// Canvas is ready when this code runs; no defensive guards needed.
const conv = useSharedCanvasPositionConversion()
// Group updates by type, then flush via each config's handler
const updatesByType = new Map<string, ElementBoundsUpdate[]>()
// Track nodes whose slots should be resynced after node size changes
const nodesNeedingSlotResync = new Set<NodeId>()
for (const entry of entries) {
if (!(entry.target instanceof HTMLElement)) continue
const element = entry.target
// Find which type this element belongs to
let elementType: string | undefined
let elementId: string | undefined
for (const [type, config] of trackingConfigs) {
const id = element.dataset[config.dataAttribute]
if (id) {
elementType = type
elementId = id
break
}
}
if (!elementType || !elementId) continue
const nodeId: NodeId | undefined =
elementType === 'node' ? elementId : undefined
// Use borderBoxSize when available; fall back to contentRect for older engines/tests
// Border box is the border included FULL wxh DOM value.
const borderBox = Array.isArray(entry.borderBoxSize)
? entry.borderBoxSize[0]
: {
inlineSize: entry.contentRect.width,
blockSize: entry.contentRect.height
}
const width = Math.max(0, borderBox.inlineSize)
const height = Math.max(0, borderBox.blockSize)
const nodeLayout = nodeId
? layoutStore.getNodeLayoutRef(nodeId).value
: null
const normalizedHeight = removeNodeTitleHeight(height)
const previousMeasurement = cachedNodeMeasurements.get(element)
const hasFreshMeasurementPending =
elementsNeedingFreshMeasurement.has(element)
const hasMatchingCachedNodeMeasurement =
previousMeasurement != null &&
previousMeasurement.nodeId === nodeId &&
nodeLayout != null &&
isBoundsEqual(previousMeasurement.bounds, nodeLayout.bounds)
// ResizeObserver emits entries where nothing changed (e.g. initial observe).
// Skip expensive DOM reads when this exact element/node already measured at
// the same normalized bounds and size.
if (
nodeLayout &&
!hasFreshMeasurementPending &&
isSizeEqual(nodeLayout.size, {
width,
height: normalizedHeight
}) &&
hasMatchingCachedNodeMeasurement
) {
continue
}
// Use existing position from layout store (source of truth) rather than
// converting screen-space getBoundingClientRect() back to canvas coords.
// The DOM→canvas conversion depends on the current canvas scale/offset,
// which can be stale during graph transitions (e.g. entering a subgraph
// before fitView runs), producing corrupted positions.
const existingPos = nodeLayout?.position
let posX: number
let posY: number
if (existingPos) {
posX = existingPos.x
posY = existingPos.y
} else {
const rect = element.getBoundingClientRect()
const [cx, cy] = conv.clientPosToCanvasPos([rect.left, rect.top])
posX = cx
posY = cy + LiteGraph.NODE_TITLE_HEIGHT
}
const bounds: Bounds = {
x: posX,
y: posY,
width,
height
}
const normalizedBounds: Bounds = {
...bounds,
height: normalizedHeight
}
elementsNeedingFreshMeasurement.delete(element)
if (nodeId) {
cachedNodeMeasurements.set(element, {
nodeId,
bounds: normalizedBounds
})
}
if (nodeLayout && isBoundsEqual(nodeLayout.bounds, normalizedBounds)) {
continue
}
let updates = updatesByType.get(elementType)
if (!updates) {
updates = []
updatesByType.set(elementType, updates)
}
updates.push({ id: elementId, bounds })
// If this entry is a node, mark it for slot layout resync
if (nodeId) {
nodesNeedingSlotResync.add(nodeId)
}
}
if (updatesByType.size === 0 && nodesNeedingSlotResync.size === 0) return
if (updatesByType.size > 0) {
layoutStore.setSource(LayoutSource.DOM)
// Flush per-type
for (const [type, updates] of updatesByType) {
const config = trackingConfigs.get(type)
if (config && updates.length) config.updateHandler(updates)
}
}
// After node bounds are updated, refresh slot cached offsets and layouts
if (nodesNeedingSlotResync.size > 0) {
for (const nodeId of nodesNeedingSlotResync) {
syncNodeSlotLayoutsFromDOM(nodeId)
}
}
})
/**
* Tracks DOM element size/position changes for a Vue component and syncs to layout store
*
* Sets up automatic ResizeObserver tracking when the component mounts and cleans up
* when unmounted. The tracked element is identified by a data attribute set on the
* component's root DOM element.
*
* @param appIdentifier - Application-level identifier for this tracked element (not a DOM ID)
* Example: node ID like 'node-123', widget ID like 'widget-456'
* @param trackingType - Type of element being tracked, determines which tracking config to use
* Example: 'node' for Vue nodes, 'widget' for UI widgets
*
* @example
* ```ts
* // Track a Vue node component with ID 'my-node-123'
* useVueElementTracking('my-node-123', 'node')
*
* // Would set data-node-id="my-node-123" on the component's root element
* // and sync size changes to layoutStore.batchUpdateNodeBounds()
* ```
*/
export function useVueElementTracking(
appIdentifier: string,
trackingType: string
) {
onMounted(() => {
const element = getCurrentInstance()?.proxy?.$el
if (!(element instanceof HTMLElement) || !appIdentifier) return
const config = trackingConfigs.get(trackingType)
if (!config) return
// Set the data attribute expected by the RO pipeline for this type
element.dataset[config.dataAttribute] = appIdentifier
markElementForFreshMeasurement(element)
resizeObserver.observe(element)
})
onUnmounted(() => {
const element = getCurrentInstance()?.proxy?.$el
if (!(element instanceof HTMLElement)) return
const config = trackingConfigs.get(trackingType)
if (!config) return
// Remove the data attribute and observer
delete element.dataset[config.dataAttribute]
cachedNodeMeasurements.delete(element)
elementsNeedingFreshMeasurement.delete(element)
deferredElements.delete(element)
resizeObserver.unobserve(element)
})
}

View File

@@ -1,10 +1,7 @@
import { computed, onUnmounted, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { Point } from '@/renderer/core/layout/types'
/**
* Composable for individual Vue node components
@@ -12,7 +9,6 @@ import type { Point } from '@/renderer/core/layout/types'
*/
export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
const nodeId = toValue(nodeIdMaybe)
const mutations = useLayoutMutations()
// Get the customRef for this node (shared write access)
const layoutRef = layoutStore.getNodeLayoutRef(nodeId)
@@ -34,21 +30,9 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
const zIndex = computed(() => layoutRef.value?.zIndex ?? 0)
/**
* Update node position directly (without drag)
*/
function moveNodeTo(position: Point) {
mutations.setSource(LayoutSource.Vue)
mutations.moveNode(nodeId, position)
}
return {
// Reactive state (via customRef)
position,
size,
zIndex,
// Mutations
moveNodeTo
zIndex
}
}