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

This reverts commit 428752619c.
This commit is contained in:
Benjamin Lu
2025-09-09 17:05:40 -07:00
parent 9786ecfb97
commit dbacbc548d
9 changed files with 293 additions and 335 deletions

View File

@@ -32,6 +32,7 @@
import {
type ComponentPublicInstance,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -41,7 +42,10 @@ 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 { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import {
type TransformState,
useDomSlotRegistration
} from '@/renderer/core/layout/slots/useDomSlotRegistration'
import SlotConnectionDot from './SlotConnectionDot.vue'
@@ -71,6 +75,11 @@ 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)
@@ -83,10 +92,11 @@ watchEffect(() => {
slotElRef.value = el || null
})
useSlotElementTracking({
useDomSlotRegistration({
nodeId: props.nodeId ?? '',
index: props.index,
slotIndex: props.index,
isInput: true,
element: slotElRef
element: slotElRef,
transform: transformState
})
</script>

View File

@@ -33,6 +33,7 @@
import {
type ComponentPublicInstance,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -42,7 +43,10 @@ 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 { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import {
type TransformState,
useDomSlotRegistration
} from '@/renderer/core/layout/slots/useDomSlotRegistration'
import SlotConnectionDot from './SlotConnectionDot.vue'
@@ -73,6 +77,11 @@ 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)
@@ -85,10 +94,11 @@ watchEffect(() => {
slotElRef.value = el || null
})
useSlotElementTracking({
useDomSlotRegistration({
nodeId: props.nodeId ?? '',
index: props.index,
slotIndex: props.index,
isInput: false,
element: slotElRef
element: slotElRef,
transform: transformState
})
</script>

View File

@@ -1,199 +0,0 @@
/**
* 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)
}
}

View File

@@ -8,20 +8,11 @@
* Supports different element types (nodes, slots, widgets, etc.) with
* customizable data attributes and update handlers.
*/
import { getCurrentInstance, inject, onMounted, onUnmounted } from 'vue'
import { getCurrentInstance, 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
*/
@@ -53,20 +44,14 @@ const trackingConfigs: Map<string, ElementTrackingConfig> = new Map([
// Single ResizeObserver instance for all Vue elements
const resizeObserver = new ResizeObserver((entries) => {
// Group updates by type, then flush via each config's handler
// Group updates by element type
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
// Identify type + id via config dataAttribute
// Find which type this element belongs to
let elementType: string | undefined
let elementId: string | undefined
@@ -81,54 +66,31 @@ const resizeObserver = new ResizeObserver((entries) => {
if (!elementType || !elementId) continue
// 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 { inlineSize: width, blockSize: height } = entry.contentBoxSize[0]
const rect = element.getBoundingClientRect()
let bounds: Bounds = { x: rect.left, y: rect.top, width, 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)
}
const bounds: Bounds = {
x: rect.left,
y: rect.top,
width,
height: height-LiteGraph.NODE_TITLE_HEIGHT
}
let updates = updatesByType.get(elementType)
if (!updates) {
updates = []
updatesByType.set(elementType, updates)
if (!updatesByType.has(elementType)) {
updatesByType.set(elementType, [])
}
const updates = updatesByType.get(elementType)
if (updates) {
updates.push({ id: elementId, bounds })
}
updates.push({ id: elementId, bounds })
}
// Flush per-type
// Process updates by type
for (const [type, updates] of updatesByType) {
const config = trackingConfigs.get(type)
if (config && updates.length) config.updateHandler(updates)
if (config && updates.length > 0) {
config.updateHandler(updates)
}
}
})
@@ -157,23 +119,16 @@ 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) 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
})
if (config) {
// Set the appropriate data attribute
element.dataset[config.dataAttribute] = appIdentifier
resizeObserver.observe(element)
}
resizeObserver.observe(element)
})
onUnmounted(() => {
@@ -181,11 +136,10 @@ export function useVueElementTracking(
if (!(element instanceof HTMLElement)) return
const config = trackingConfigs.get(trackingType)
if (!config) return
// Remove the data attribute and observer
delete element.dataset[config.dataAttribute]
resizeObserver.unobserve(element)
elementConversion.delete(element)
if (config) {
// Remove the data attribute
delete element.dataset[config.dataAttribute]
resizeObserver.unobserve(element)
}
})
}

View File

@@ -6,7 +6,6 @@
*/
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'
@@ -20,7 +19,12 @@ export function useNodeLayout(nodeId: string) {
const mutations = useLayoutMutations()
// Get transform utilities from TransformPane if available
const transformState = inject(TransformStateKey)
const transformState = inject('transformState') as
| {
canvasToScreen: (point: Point) => Point
screenToCanvas: (point: Point) => Point
}
| undefined
// Get the customRef for this node (shared write access)
const layoutRef = store.getNodeLayoutRef(nodeId)