Files
ComfyUI_frontend/src/composables/graph/useNodeChangeDetection.ts
Christian Byrne 4f337be837 feat: Implement CRDT-based layout system for Vue nodes (#4959)
* feat: Implement CRDT-based layout system for Vue nodes

Major refactor to solve snap-back issues and create single source of truth for node positions:

- Add Yjs-based CRDT layout store for conflict-free position management
- Implement layout mutations service with clean API
- Create Vue composables for layout access and node dragging
- Add one-way sync from layout store to LiteGraph
- Disable LiteGraph dragging when Vue nodes mode is enabled
- Add z-index management with bring-to-front on node interaction
- Add comprehensive TypeScript types for layout system
- Include unit tests for layout store operations
- Update documentation to reflect CRDT architecture

This provides a solid foundation for both single-user performance and future real-time collaboration features.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: Apply linter fixes to layout system

* fix: Remove unnecessary README files and revert services README

- Remove unnecessary types/README.md file
- Revert unrelated changes to services/README.md
- Keep only relevant documentation for the layout system implementation

These were issues identified during PR review that needed to be addressed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Clean up layout store and implement proper CRDT operations

- Created dedicated layoutOperations.ts with production-grade CRDT interfaces
- Integrated existing QuadTree spatial index instead of simple cache
- Split composables into separate files (useLayout, useNodeLayout, useLayoutSync)
- Cleaned up operation handlers using specific types instead of Extract
- Added proper operation interfaces with type guards and extensibility
- Updated all type references to use new operation structure

The layout store now properly uses the existing QuadTree infrastructure for
efficient spatial queries and follows CRDT best practices with well-defined
operation interfaces.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Extract services and split composables for better organization

- Created SpatialIndexManager to handle QuadTree operations separately
- Added LayoutAdapter interface for CRDT abstraction (Yjs, mock implementations)
- Split GraphNodeManager into focused composables:
  - useNodeWidgets: Widget state and callback management
  - useNodeChangeDetection: RAF-based geometry change detection
  - useNodeState: Node visibility and reactive state management
- Extracted constants for magic numbers and configuration values
- Updated layout store to use SpatialIndexManager and constants

This improves code organization, testability, and makes it easier to swap
CRDT implementations or mock services for testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add node slots to layout tree

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* Remove slots from layoutTypes

* Totally not scuffed renderer and adapter

* Revert "Totally not scuffed renderer and adapter"

This reverts commit 2b9d83efb8.

* Revert "Remove slots from layoutTypes"

This reverts commit 18f78ff786.

* Reapply "Add node slots to layout tree"

This reverts commit 236fecb549.

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* docs: Replace architecture docs with comprehensive ADR

- Add ADR-0002 for CRDT-based layout system decision
- Follow established ADR template with persuasive reasoning
- Include performance benefits, collaboration readiness, and architectural advantages
- Update ADR index

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
2025-08-16 20:49:17 -07:00

181 lines
4.3 KiB
TypeScript

/**
* Node Change Detection
*
* RAF-based change detection for node positions and sizes.
* Syncs LiteGraph changes to the layout system.
*/
import { reactive } from 'vue'
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { layoutMutations } from '@/services/layoutMutations'
export interface ChangeDetectionMetrics {
updateTime: number
positionUpdates: number
sizeUpdates: number
rafUpdateCount: number
}
/**
* Change detection for node geometry
*/
export function useNodeChangeDetection(graph: LGraph) {
const metrics = reactive<ChangeDetectionMetrics>({
updateTime: 0,
positionUpdates: 0,
sizeUpdates: 0,
rafUpdateCount: 0
})
// Track last known positions/sizes
const lastSnapshot = new Map<
string,
{ pos: [number, number]; size: [number, number] }
>()
/**
* Detects position changes for a single node
*/
const detectPositionChanges = (
node: LGraphNode,
nodePositions: Map<string, { x: number; y: number }>
): boolean => {
const id = String(node.id)
const currentPos = nodePositions.get(id)
if (
!currentPos ||
currentPos.x !== node.pos[0] ||
currentPos.y !== node.pos[1]
) {
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
// Push position change to layout store
void layoutMutations.moveNode(id, { x: node.pos[0], y: node.pos[1] })
return true
}
return false
}
/**
* Detects size changes for a single node
*/
const detectSizeChanges = (
node: LGraphNode,
nodeSizes: Map<string, { width: number; height: number }>
): boolean => {
const id = String(node.id)
const currentSize = nodeSizes.get(id)
if (
!currentSize ||
currentSize.width !== node.size[0] ||
currentSize.height !== node.size[1]
) {
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
// Push size change to layout store
void layoutMutations.resizeNode(id, {
width: node.size[0],
height: node.size[1]
})
return true
}
return false
}
/**
* Main RAF change detection function
*/
const detectChanges = (
nodePositions: Map<string, { x: number; y: number }>,
nodeSizes: Map<string, { width: number; height: number }>,
onSpatialChange?: (node: LGraphNode, id: string) => void
) => {
const startTime = performance.now()
if (!graph?._nodes) return
let positionUpdates = 0
let sizeUpdates = 0
// Set source for all canvas-driven updates
layoutMutations.setSource('canvas')
// Process each node for changes
for (const node of graph._nodes) {
const id = String(node.id)
const posChanged = detectPositionChanges(node, nodePositions)
const sizeChanged = detectSizeChanges(node, nodeSizes)
if (posChanged) positionUpdates++
if (sizeChanged) sizeUpdates++
// Notify spatial change if needed
if ((posChanged || sizeChanged) && onSpatialChange) {
onSpatialChange(node, id)
}
}
// Update metrics
const endTime = performance.now()
metrics.updateTime = endTime - startTime
metrics.positionUpdates = positionUpdates
metrics.sizeUpdates = sizeUpdates
if (positionUpdates > 0 || sizeUpdates > 0) {
metrics.rafUpdateCount++
}
}
/**
* Take a snapshot of current node positions/sizes
*/
const takeSnapshot = () => {
if (!graph?._nodes) return
lastSnapshot.clear()
for (const node of graph._nodes) {
lastSnapshot.set(String(node.id), {
pos: [node.pos[0], node.pos[1]],
size: [node.size[0], node.size[1]]
})
}
}
/**
* Check if any nodes have changed since last snapshot
*/
const hasChangedSinceSnapshot = (): boolean => {
if (!graph?._nodes) return false
for (const node of graph._nodes) {
const id = String(node.id)
const last = lastSnapshot.get(id)
if (!last) continue
if (
last.pos[0] !== node.pos[0] ||
last.pos[1] !== node.pos[1] ||
last.size[0] !== node.size[0] ||
last.size[1] !== node.size[1]
) {
return true
}
}
return false
}
return {
metrics,
detectChanges,
detectPositionChanges,
detectSizeChanges,
takeSnapshot,
hasChangedSinceSnapshot
}
}