mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 18:24:11 +00:00
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 commit460493a620. * Remove slots from layoutTypes * Totally not scuffed renderer and adapter * Revert "Totally not scuffed renderer and adapter" This reverts commit2b9d83efb8. * Revert "Remove slots from layoutTypes" This reverts commit18f78ff786. * Reapply "Add node slots to layout tree" This reverts commit236fecb549. * Revert "Add node slots to layout tree" This reverts commit460493a620. * 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>
This commit is contained in:
180
src/composables/graph/useNodeChangeDetection.ts
Normal file
180
src/composables/graph/useNodeChangeDetection.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user