Refactor vue slot tracking (#5463)

* add dom element resize observer registry for vue node components

* Update src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts

Co-authored-by: AustinMroz <austin@comfy.org>

* 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 110ecf31da.

* Revert "refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeObserver sizing, centralized slot tracking, and small readability updates"

This reverts commit 428752619c.

* [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 <noreply@anthropic.com>

* 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

* refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeObserver sizing, centralized slot tracking, and small readability updates

* Fix conversion

* Readd padding

* revert churn reducings from layoutStore.ts

* Rely on RO for resize, and batch

* Improve churn

* Cache canvas offset

* rename from measure

* remove unused

* address review comments

* Update legacy injection

* nit

* Split into store

* nit

* perf improvement

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Benjamin Lu
2025-09-16 19:28:04 -07:00
committed by GitHub
parent 6b59f839e0
commit ff5d0923ca
21 changed files with 498 additions and 379 deletions

View File

@@ -4,15 +4,18 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json'
import { useDomSlotRegistration } from '@/renderer/core/layout/slots/useDomSlotRegistration'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import InputSlot from './InputSlot.vue'
import OutputSlot from './OutputSlot.vue'
// Mock composable used by InputSlot/OutputSlot so we can assert call params
vi.mock('@/renderer/core/layout/slots/useDomSlotRegistration', () => ({
useDomSlotRegistration: vi.fn(() => ({ remeasure: vi.fn() }))
}))
vi.mock(
'@/renderer/extensions/vueNodes/composables/useSlotElementTracking',
() => ({
useSlotElementTracking: vi.fn(() => ({ stop: vi.fn() }))
})
)
type InputSlotProps = ComponentMountingOptions<typeof InputSlot>['props']
type OutputSlotProps = ComponentMountingOptions<typeof OutputSlot>['props']
@@ -49,7 +52,7 @@ const mountOutputSlot = (props: OutputSlotProps) =>
describe('InputSlot/OutputSlot', () => {
beforeEach(() => {
vi.mocked(useDomSlotRegistration).mockClear()
vi.mocked(useSlotElementTracking).mockClear()
})
it('InputSlot registers with correct options', () => {
@@ -59,11 +62,11 @@ describe('InputSlot/OutputSlot', () => {
slotData: { name: 'A', type: 'any', boundingRect: [0, 0, 0, 0] }
})
expect(useDomSlotRegistration).toHaveBeenLastCalledWith(
expect(useSlotElementTracking).toHaveBeenLastCalledWith(
expect.objectContaining({
nodeId: 'node-1',
slotIndex: 3,
isInput: true
index: 3,
type: 'input'
})
)
})
@@ -75,11 +78,11 @@ describe('InputSlot/OutputSlot', () => {
slotData: { name: 'B', type: 'any', boundingRect: [0, 0, 0, 0] }
})
expect(useDomSlotRegistration).toHaveBeenLastCalledWith(
expect(useSlotElementTracking).toHaveBeenLastCalledWith(
expect.objectContaining({
nodeId: 'node-2',
slotIndex: 1,
isInput: false
index: 1,
type: 'output'
})
)
})

View File

@@ -32,7 +32,6 @@
import {
type ComponentPublicInstance,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -41,11 +40,7 @@ import {
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 +70,6 @@ onErrorCaptured((error) => {
// Get slot color based on type
const slotColor = computed(() => getSlotColor(props.slotData.type))
const transformState = inject<TransformState | undefined>(
'transformState',
undefined
)
const connectionDotRef = ref<ComponentPublicInstance<{
slotElRef: HTMLElement | undefined
}> | null>(null)
@@ -92,11 +82,10 @@ watchEffect(() => {
slotElRef.value = el || null
})
useDomSlotRegistration({
useSlotElementTracking({
nodeId: props.nodeId ?? '',
slotIndex: props.index,
isInput: true,
element: slotElRef,
transform: transformState
index: props.index,
type: 'input',
element: slotElRef
})
</script>

View File

@@ -147,6 +147,7 @@ import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
@@ -212,19 +213,7 @@ if (!selectedNodeIds) {
}
// Inject transform state for coordinate conversion
const transformState = inject('transformState') as
| {
camera: { z: number }
canvasToScreen: (point: { x: number; y: number }) => {
x: number
y: number
}
screenToCanvas: (point: { x: number; y: number }) => {
x: number
y: number
}
}
| undefined
const transformState = inject(TransformStateKey)
// Computed selection state - only this node re-evaluates when its selection changes
const isSelected = computed(() => {
@@ -281,7 +270,7 @@ const {
} = useNodeLayout(nodeData.id)
onMounted(() => {
if (size && transformState) {
if (size && transformState?.camera) {
const scale = transformState.camera.z
const screenSize = {
width: size.width * scale,

View File

@@ -33,7 +33,6 @@
import {
type ComponentPublicInstance,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -42,11 +41,7 @@ import {
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 +72,6 @@ onErrorCaptured((error) => {
// Get slot color based on type
const slotColor = computed(() => getSlotColor(props.slotData.type))
const transformState = inject<TransformState | undefined>(
'transformState',
undefined
)
const connectionDotRef = ref<ComponentPublicInstance<{
slotElRef: HTMLElement | undefined
}> | null>(null)
@@ -94,11 +84,10 @@ watchEffect(() => {
slotElRef.value = el || null
})
useDomSlotRegistration({
useSlotElementTracking({
nodeId: props.nodeId ?? '',
slotIndex: props.index,
isInput: false,
element: slotElRef,
transform: transformState
index: props.index,
type: 'output',
element: slotElRef
})
</script>