mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 23:04:06 +00:00
* 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>
183 lines
4.4 KiB
TypeScript
183 lines
4.4 KiB
TypeScript
/**
|
|
* Node Widget Management
|
|
*
|
|
* Handles widget state synchronization between LiteGraph and Vue.
|
|
* Provides wrapped callbacks to maintain consistency.
|
|
*/
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { WidgetValue } from '@/types/simplifiedWidget'
|
|
|
|
export type { WidgetValue }
|
|
|
|
export interface SafeWidgetData {
|
|
name: string
|
|
type: string
|
|
value: WidgetValue
|
|
options?: Record<string, unknown>
|
|
callback?: ((value: unknown) => void) | undefined
|
|
}
|
|
|
|
export interface VueNodeData {
|
|
id: string
|
|
title: string
|
|
type: string
|
|
mode: number
|
|
selected: boolean
|
|
executing: boolean
|
|
widgets?: SafeWidgetData[]
|
|
inputs?: unknown[]
|
|
outputs?: unknown[]
|
|
flags?: {
|
|
collapsed?: boolean
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates that a value is a valid WidgetValue type
|
|
*/
|
|
export function validateWidgetValue(value: unknown): WidgetValue {
|
|
if (value === null || value === undefined || value === void 0) {
|
|
return undefined
|
|
}
|
|
if (
|
|
typeof value === 'string' ||
|
|
typeof value === 'number' ||
|
|
typeof value === 'boolean'
|
|
) {
|
|
return value
|
|
}
|
|
if (typeof value === 'object') {
|
|
// Check if it's a File array
|
|
if (Array.isArray(value) && value.every((item) => item instanceof File)) {
|
|
return value as File[]
|
|
}
|
|
// Otherwise it's a generic object
|
|
return value as object
|
|
}
|
|
// If none of the above, return undefined
|
|
console.warn(`Invalid widget value type: ${typeof value}`, value)
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Extract safe widget data from LiteGraph widgets
|
|
*/
|
|
export function extractWidgetData(
|
|
widgets?: any[]
|
|
): SafeWidgetData[] | undefined {
|
|
if (!widgets) return undefined
|
|
|
|
return widgets.map((widget) => {
|
|
try {
|
|
let value = widget.value
|
|
|
|
// For combo widgets, if value is undefined, use the first option as default
|
|
if (
|
|
value === undefined &&
|
|
widget.type === 'combo' &&
|
|
widget.options?.values &&
|
|
Array.isArray(widget.options.values) &&
|
|
widget.options.values.length > 0
|
|
) {
|
|
value = widget.options.values[0]
|
|
}
|
|
|
|
return {
|
|
name: widget.name,
|
|
type: widget.type,
|
|
value: validateWidgetValue(value),
|
|
options: widget.options ? { ...widget.options } : undefined,
|
|
callback: widget.callback
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
name: widget.name || 'unknown',
|
|
type: widget.type || 'text',
|
|
value: undefined,
|
|
options: undefined,
|
|
callback: undefined
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Widget callback management for LiteGraph/Vue sync
|
|
*/
|
|
export function useNodeWidgets() {
|
|
/**
|
|
* Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync
|
|
*/
|
|
const createWrappedCallback = (
|
|
widget: { value?: unknown; name: string },
|
|
originalCallback: ((value: unknown) => void) | undefined,
|
|
nodeId: string,
|
|
onUpdate: (nodeId: string, widgetName: string, value: unknown) => void
|
|
) => {
|
|
let updateInProgress = false
|
|
|
|
return (value: unknown) => {
|
|
if (updateInProgress) return
|
|
updateInProgress = true
|
|
|
|
try {
|
|
// Validate that the value is of an acceptable type
|
|
if (
|
|
value !== null &&
|
|
value !== undefined &&
|
|
typeof value !== 'string' &&
|
|
typeof value !== 'number' &&
|
|
typeof value !== 'boolean' &&
|
|
typeof value !== 'object'
|
|
) {
|
|
console.warn(`Invalid widget value type: ${typeof value}`)
|
|
updateInProgress = false
|
|
return
|
|
}
|
|
|
|
// Always update widget.value to ensure sync
|
|
widget.value = value
|
|
|
|
// Call the original callback if it exists
|
|
if (originalCallback) {
|
|
originalCallback.call(widget, value)
|
|
}
|
|
|
|
// Update Vue state to maintain synchronization
|
|
onUpdate(nodeId, widget.name, value)
|
|
} finally {
|
|
updateInProgress = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up widget callbacks for a node
|
|
*/
|
|
const setupNodeWidgetCallbacks = (
|
|
node: LGraphNode,
|
|
onUpdate: (nodeId: string, widgetName: string, value: unknown) => void
|
|
) => {
|
|
if (!node.widgets) return
|
|
|
|
const nodeId = String(node.id)
|
|
|
|
node.widgets.forEach((widget) => {
|
|
const originalCallback = widget.callback
|
|
widget.callback = createWrappedCallback(
|
|
widget,
|
|
originalCallback,
|
|
nodeId,
|
|
onUpdate
|
|
)
|
|
})
|
|
}
|
|
|
|
return {
|
|
validateWidgetValue,
|
|
extractWidgetData,
|
|
createWrappedCallback,
|
|
setupNodeWidgetCallbacks
|
|
}
|
|
}
|