Files
ComfyUI_frontend/src/services/spatialIndexManager.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

167 lines
3.6 KiB
TypeScript

/**
* Spatial Index Manager
*
* Manages spatial indexing for efficient node queries based on bounds.
* Uses QuadTree for fast spatial lookups with caching for performance.
*/
import { PERFORMANCE_CONFIG, QUADTREE_CONFIG } from '@/constants/layout'
import type { Bounds, NodeId } from '@/types/layoutTypes'
import { QuadTree } from '@/utils/spatial/QuadTree'
/**
* Cache entry for spatial queries
*/
interface CacheEntry {
result: NodeId[]
timestamp: number
}
/**
* Spatial index manager using QuadTree
*/
export class SpatialIndexManager {
private quadTree: QuadTree<NodeId>
private queryCache: Map<string, CacheEntry>
private cacheSize = 0
constructor(bounds?: Bounds) {
this.quadTree = new QuadTree<NodeId>(
bounds ?? QUADTREE_CONFIG.DEFAULT_BOUNDS,
{
maxDepth: QUADTREE_CONFIG.MAX_DEPTH,
maxItemsPerNode: QUADTREE_CONFIG.MAX_ITEMS_PER_NODE
}
)
this.queryCache = new Map()
}
/**
* Insert a node into the spatial index
*/
insert(nodeId: NodeId, bounds: Bounds): void {
this.quadTree.insert(nodeId, bounds, nodeId)
this.invalidateCache()
}
/**
* Update a node's bounds in the spatial index
*/
update(nodeId: NodeId, bounds: Bounds): void {
this.quadTree.update(nodeId, bounds)
this.invalidateCache()
}
/**
* Remove a node from the spatial index
*/
remove(nodeId: NodeId): void {
this.quadTree.remove(nodeId)
this.invalidateCache()
}
/**
* Query nodes within the given bounds
*/
query(bounds: Bounds): NodeId[] {
const cacheKey = this.getCacheKey(bounds)
const cached = this.queryCache.get(cacheKey)
// Check cache validity
if (cached) {
const age = Date.now() - cached.timestamp
if (age < PERFORMANCE_CONFIG.SPATIAL_CACHE_TTL) {
return cached.result
}
// Remove stale entry
this.queryCache.delete(cacheKey)
this.cacheSize--
}
// Perform query
const result = this.quadTree.query(bounds)
// Cache result
this.addToCache(cacheKey, result)
return result
}
/**
* Clear all nodes from the spatial index
*/
clear(): void {
this.quadTree.clear()
this.invalidateCache()
}
/**
* Get the current size of the index
*/
get size(): number {
return this.quadTree.size
}
/**
* Get debug information about the spatial index
*/
getDebugInfo() {
return {
quadTreeInfo: this.quadTree.getDebugInfo(),
cacheSize: this.cacheSize,
cacheEntries: this.queryCache.size
}
}
/**
* Generate cache key for bounds
*/
private getCacheKey(bounds: Bounds): string {
return `${bounds.x},${bounds.y},${bounds.width},${bounds.height}`
}
/**
* Add result to cache with LRU eviction
*/
private addToCache(key: string, result: NodeId[]): void {
// Evict oldest entries if cache is full
if (this.cacheSize >= PERFORMANCE_CONFIG.SPATIAL_CACHE_MAX_SIZE) {
const oldestKey = this.findOldestCacheEntry()
if (oldestKey) {
this.queryCache.delete(oldestKey)
this.cacheSize--
}
}
this.queryCache.set(key, {
result,
timestamp: Date.now()
})
this.cacheSize++
}
/**
* Find oldest cache entry for LRU eviction
*/
private findOldestCacheEntry(): string | null {
let oldestKey: string | null = null
let oldestTime = Infinity
for (const [key, entry] of this.queryCache) {
if (entry.timestamp < oldestTime) {
oldestTime = entry.timestamp
oldestKey = key
}
}
return oldestKey
}
/**
* Invalidate all cached queries
*/
private invalidateCache(): void {
this.queryCache.clear()
this.cacheSize = 0
}
}