mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeObserver sizing, centralized slot tracking, and small readability updates
This commit is contained in:
@@ -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,
|
||||
|
||||
18
src/renderer/core/layout/injectionKeys.ts
Normal file
18
src/renderer/core/layout/injectionKeys.ts
Normal file
@@ -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<number>,
|
||||
nodeSize: ArrayLike<number>,
|
||||
viewport: { width: number; height: number },
|
||||
margin?: number
|
||||
) => boolean
|
||||
}
|
||||
|
||||
export const TransformStateKey: InjectionKey<TransformState> =
|
||||
Symbol('transformState')
|
||||
@@ -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<object>()
|
||||
|
||||
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<HTMLElement | null>,
|
||||
{
|
||||
stopWatcher?: WatchStopHandle
|
||||
handleResize?: () => void
|
||||
}
|
||||
>()
|
||||
|
||||
interface SlotRegistrationOptions {
|
||||
nodeId: string
|
||||
slotIndex: number
|
||||
isInput: boolean
|
||||
element: Ref<HTMLElement | null>
|
||||
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<LayoutPoint | null>(null)
|
||||
const lastMeasuredBounds = ref<DOMRect | null>(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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>(
|
||||
'transformState',
|
||||
undefined
|
||||
)
|
||||
|
||||
const connectionDotRef = ref<ComponentPublicInstance<{
|
||||
slotElRef: HTMLElement | undefined
|
||||
}> | 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
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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>(
|
||||
'transformState',
|
||||
undefined
|
||||
)
|
||||
|
||||
const connectionDotRef = ref<ComponentPublicInstance<{
|
||||
slotElRef: HTMLElement | undefined
|
||||
}> | 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
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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<string, SlotEntry>
|
||||
stopWatch?: () => void
|
||||
}
|
||||
|
||||
// Registry of nodes and their slots
|
||||
const nodeRegistry = new Map<string, NodeEntry>()
|
||||
|
||||
// RAF batching
|
||||
const pendingNodes = new Set<string>()
|
||||
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<HTMLElement | null>
|
||||
}) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<string, ElementTrackingConfig> = 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<string, Array<{ id: string; bounds: Bounds }>>()
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user