mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
@@ -11,16 +11,12 @@ import * as Y from 'yjs'
|
||||
import { ACTOR_CONFIG, DEBUG_CONFIG } from '@/constants/layout'
|
||||
import { SpatialIndexManager } from '@/services/spatialIndexManager'
|
||||
import type {
|
||||
BatchUpdateSlotsOperation,
|
||||
CreateNodeOperation,
|
||||
CreateSlotOperation,
|
||||
DeleteNodeOperation,
|
||||
DeleteSlotOperation,
|
||||
LayoutOperation,
|
||||
MoveNodeOperation,
|
||||
ResizeNodeOperation,
|
||||
SetNodeZIndexOperation,
|
||||
UpdateSlotOperation
|
||||
SetNodeZIndexOperation
|
||||
} from '@/types/layoutOperations'
|
||||
import type {
|
||||
Bounds,
|
||||
@@ -28,9 +24,7 @@ import type {
|
||||
LayoutStore,
|
||||
NodeId,
|
||||
NodeLayout,
|
||||
Point,
|
||||
SlotId,
|
||||
SlotLayout
|
||||
Point
|
||||
} from '@/types/layoutTypes'
|
||||
|
||||
// Create logger for layout store
|
||||
@@ -44,7 +38,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
// Yjs document and shared data structures
|
||||
private ydoc = new Y.Doc()
|
||||
private ynodes: Y.Map<Y.Map<unknown>> // Maps nodeId -> Y.Map containing NodeLayout data
|
||||
private yslots: Y.Map<Y.Map<unknown>> // Maps slotId -> Y.Map containing SlotLayout data
|
||||
private yoperations: Y.Array<LayoutOperation> // Operation log
|
||||
|
||||
// Vue reactivity layer
|
||||
@@ -61,8 +54,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
// CustomRef cache and trigger functions
|
||||
private nodeRefs = new Map<NodeId, Ref<NodeLayout | null>>()
|
||||
private nodeTriggers = new Map<NodeId, () => void>()
|
||||
private slotRefs = new Map<SlotId, Ref<SlotLayout | null>>()
|
||||
private slotTriggers = new Map<SlotId, () => void>()
|
||||
|
||||
// Spatial index manager
|
||||
private spatialIndex: SpatialIndexManager
|
||||
@@ -70,7 +61,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
constructor() {
|
||||
// Initialize Yjs data structures
|
||||
this.ynodes = this.ydoc.getMap('nodes')
|
||||
this.yslots = this.ydoc.getMap('slots')
|
||||
this.yoperations = this.ydoc.getArray('operations')
|
||||
|
||||
// Initialize spatial index manager
|
||||
@@ -90,20 +80,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
})
|
||||
})
|
||||
|
||||
// Listen for slot changes
|
||||
this.yslots.observe((event) => {
|
||||
this.version++
|
||||
|
||||
// Trigger all affected slot refs
|
||||
event.changes.keys.forEach((_change, key) => {
|
||||
const trigger = this.slotTriggers.get(key)
|
||||
if (trigger) {
|
||||
logger.debug(`Yjs change detected for slot ${key}, triggering ref`)
|
||||
trigger()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Debug: Log layout operations
|
||||
if (localStorage.getItem(DEBUG_CONFIG.LAYOUT_DEBUG_KEY) === 'true') {
|
||||
this.yoperations.observe((event) => {
|
||||
@@ -272,140 +248,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a customRef for a slot layout
|
||||
*/
|
||||
getSlotLayoutRef(slotId: SlotId): Ref<SlotLayout | null> {
|
||||
let slotRef = this.slotRefs.get(slotId)
|
||||
|
||||
if (!slotRef) {
|
||||
logger.debug(`Creating new layout ref for slot ${slotId}`)
|
||||
|
||||
slotRef = customRef<SlotLayout | null>((track, trigger) => {
|
||||
// Store the trigger so we can call it when Yjs changes
|
||||
this.slotTriggers.set(slotId, trigger)
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
track()
|
||||
const yslot = this.yslots.get(slotId)
|
||||
const layout = yslot ? this.ySlotToLayout(yslot) : null
|
||||
logger.debug(`Layout ref GET for slot ${slotId}:`, {
|
||||
position: layout?.position,
|
||||
hasYslot: !!yslot,
|
||||
version: this.version
|
||||
})
|
||||
return layout
|
||||
},
|
||||
set: (newLayout: SlotLayout | null) => {
|
||||
if (newLayout === null) {
|
||||
// Delete operation
|
||||
const existing = this.yslots.get(slotId)
|
||||
if (existing) {
|
||||
this.applyOperation({
|
||||
type: 'deleteSlot',
|
||||
slotId,
|
||||
timestamp: Date.now(),
|
||||
source: this.currentSource,
|
||||
actor: this.currentActor,
|
||||
previousLayout: this.ySlotToLayout(existing)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Update or create operation
|
||||
const existing = this.yslots.get(slotId)
|
||||
if (!existing) {
|
||||
// Create operation
|
||||
this.applyOperation({
|
||||
type: 'createSlot',
|
||||
slotId,
|
||||
layout: newLayout,
|
||||
timestamp: Date.now(),
|
||||
source: this.currentSource,
|
||||
actor: this.currentActor
|
||||
})
|
||||
} else {
|
||||
const existingLayout = this.ySlotToLayout(existing)
|
||||
// Update position if changed
|
||||
if (
|
||||
existingLayout.position.x !== newLayout.position.x ||
|
||||
existingLayout.position.y !== newLayout.position.y
|
||||
) {
|
||||
this.applyOperation({
|
||||
type: 'updateSlot',
|
||||
slotId,
|
||||
position: newLayout.position,
|
||||
previousPosition: existingLayout.position,
|
||||
timestamp: Date.now(),
|
||||
source: this.currentSource,
|
||||
actor: this.currentActor
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug(`Layout ref SET triggering for slot ${slotId}`)
|
||||
trigger()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.slotRefs.set(slotId, slotRef)
|
||||
}
|
||||
|
||||
return slotRef
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slots for a specific node (reactive)
|
||||
*/
|
||||
getNodeSlots(nodeId: NodeId): ComputedRef<SlotLayout[]> {
|
||||
return computed(() => {
|
||||
// Touch version for reactivity
|
||||
void this.version
|
||||
|
||||
const result: SlotLayout[] = []
|
||||
for (const [slotId] of this.yslots) {
|
||||
const yslot = this.yslots.get(slotId)
|
||||
if (yslot) {
|
||||
const layout = this.ySlotToLayout(yslot)
|
||||
if (layout && layout.nodeId === nodeId) {
|
||||
result.push(layout)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort by type and index
|
||||
result.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === 'input' ? -1 : 1
|
||||
}
|
||||
return a.index - b.index
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all slots as a reactive map
|
||||
*/
|
||||
getAllSlots(): ComputedRef<ReadonlyMap<SlotId, SlotLayout>> {
|
||||
return computed(() => {
|
||||
// Touch version for reactivity
|
||||
void this.version
|
||||
|
||||
const result = new Map<SlotId, SlotLayout>()
|
||||
for (const [slotId] of this.yslots) {
|
||||
const yslot = this.yslots.get(slotId)
|
||||
if (yslot) {
|
||||
const layout = this.ySlotToLayout(yslot)
|
||||
if (layout) {
|
||||
result.set(slotId, layout)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version for change detection
|
||||
*/
|
||||
@@ -448,54 +290,13 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
return this.spatialIndex.query(bounds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query slot at point (non-reactive for performance)
|
||||
*/
|
||||
querySlotAtPoint(point: Point): SlotId | null {
|
||||
// First find the node at the point
|
||||
const nodeId = this.queryNodeAtPoint(point)
|
||||
if (!nodeId) return null
|
||||
|
||||
// Then check slots for that node
|
||||
for (const [slotId] of this.yslots) {
|
||||
const yslot = this.yslots.get(slotId)
|
||||
if (yslot) {
|
||||
const slot = this.ySlotToLayout(yslot)
|
||||
if (slot && slot.nodeId === nodeId) {
|
||||
const ynode = this.ynodes.get(nodeId)
|
||||
if (ynode) {
|
||||
const node = this.yNodeToLayout(ynode)
|
||||
// Convert slot relative position to absolute
|
||||
const absoluteX = node.position.x + slot.position.x
|
||||
const absoluteY = node.position.y + slot.position.y
|
||||
// Check if point is within slot radius (typically 10-15 pixels)
|
||||
const slotRadius = 15
|
||||
const dx = point.x - absoluteX
|
||||
const dy = point.y - absoluteY
|
||||
if (dx * dx + dy * dy <= slotRadius * slotRadius) {
|
||||
return slotId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a layout operation using Yjs transactions
|
||||
*/
|
||||
applyOperation(operation: LayoutOperation): void {
|
||||
const entityId =
|
||||
'nodeId' in operation
|
||||
? operation.nodeId
|
||||
: 'slotId' in operation
|
||||
? (operation as any).slotId
|
||||
: 'unknown'
|
||||
|
||||
logger.debug(`applyOperation called:`, {
|
||||
type: operation.type,
|
||||
entityId,
|
||||
nodeId: operation.nodeId,
|
||||
operation
|
||||
})
|
||||
|
||||
@@ -544,21 +345,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
case 'deleteNode':
|
||||
this.handleDeleteNode(operation as DeleteNodeOperation, change)
|
||||
break
|
||||
case 'createSlot':
|
||||
this.handleCreateSlot(operation as CreateSlotOperation, change)
|
||||
break
|
||||
case 'updateSlot':
|
||||
this.handleUpdateSlot(operation as UpdateSlotOperation, change)
|
||||
break
|
||||
case 'deleteSlot':
|
||||
this.handleDeleteSlot(operation as DeleteSlotOperation, change)
|
||||
break
|
||||
case 'batchUpdateSlots':
|
||||
this.handleBatchUpdateSlots(
|
||||
operation as BatchUpdateSlotsOperation,
|
||||
change
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,87 +548,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
change.nodeIds.push(operation.nodeId)
|
||||
}
|
||||
|
||||
// Slot operation handlers
|
||||
private handleCreateSlot(
|
||||
operation: CreateSlotOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
const yslot = this.layoutToYSlot(operation.layout)
|
||||
this.yslots.set(operation.slotId, yslot)
|
||||
|
||||
change.type = 'create'
|
||||
// Track the affected node
|
||||
change.nodeIds.push(operation.layout.nodeId)
|
||||
}
|
||||
|
||||
private handleUpdateSlot(
|
||||
operation: UpdateSlotOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
const yslot = this.yslots.get(operation.slotId)
|
||||
if (!yslot) {
|
||||
logger.warn(`No yslot found for ${operation.slotId}`)
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug(`Updating slot ${operation.slotId}`, operation.position)
|
||||
yslot.set('position', operation.position)
|
||||
|
||||
// Track the affected node
|
||||
const nodeId = yslot.get('nodeId') as string
|
||||
if (nodeId) {
|
||||
change.nodeIds.push(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeleteSlot(
|
||||
operation: DeleteSlotOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
const yslot = this.yslots.get(operation.slotId)
|
||||
if (!yslot) return
|
||||
|
||||
// Track the affected node before deletion
|
||||
const nodeId = yslot.get('nodeId') as string
|
||||
|
||||
this.yslots.delete(operation.slotId)
|
||||
this.slotRefs.delete(operation.slotId)
|
||||
this.slotTriggers.delete(operation.slotId)
|
||||
|
||||
change.type = 'delete'
|
||||
if (nodeId) {
|
||||
change.nodeIds.push(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
private handleBatchUpdateSlots(
|
||||
operation: BatchUpdateSlotsOperation,
|
||||
change: LayoutChange
|
||||
): void {
|
||||
// Delete all existing slots for this node
|
||||
const slotsToDelete: string[] = []
|
||||
for (const [slotId] of this.yslots) {
|
||||
const yslot = this.yslots.get(slotId)
|
||||
if (yslot && yslot.get('nodeId') === operation.nodeId) {
|
||||
slotsToDelete.push(slotId)
|
||||
}
|
||||
}
|
||||
|
||||
slotsToDelete.forEach((slotId) => {
|
||||
this.yslots.delete(slotId)
|
||||
this.slotRefs.delete(slotId)
|
||||
this.slotTriggers.delete(slotId)
|
||||
})
|
||||
|
||||
// Add new slots
|
||||
operation.slots.forEach((slotLayout) => {
|
||||
const yslot = this.layoutToYSlot(slotLayout)
|
||||
this.yslots.set(slotLayout.id, yslot)
|
||||
})
|
||||
|
||||
change.nodeIds.push(operation.nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update node bounds helper
|
||||
*/
|
||||
@@ -882,26 +587,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
}
|
||||
|
||||
private layoutToYSlot(layout: SlotLayout): Y.Map<unknown> {
|
||||
const yslot = new Y.Map<unknown>()
|
||||
yslot.set('id', layout.id)
|
||||
yslot.set('nodeId', layout.nodeId)
|
||||
yslot.set('position', layout.position)
|
||||
yslot.set('type', layout.type)
|
||||
yslot.set('index', layout.index)
|
||||
return yslot
|
||||
}
|
||||
|
||||
private ySlotToLayout(yslot: Y.Map<unknown>): SlotLayout {
|
||||
return {
|
||||
id: yslot.get('id') as string,
|
||||
nodeId: yslot.get('nodeId') as string,
|
||||
position: yslot.get('position') as Point,
|
||||
type: yslot.get('type') as 'input' | 'output',
|
||||
index: yslot.get('index') as number
|
||||
}
|
||||
}
|
||||
|
||||
private notifyChange(change: LayoutChange): void {
|
||||
this.changeListeners.forEach((listener) => {
|
||||
try {
|
||||
|
||||
@@ -8,13 +8,7 @@
|
||||
* - Conflict resolution (CRDT)
|
||||
* - Debugging (actor, timestamp, source)
|
||||
*/
|
||||
import type {
|
||||
NodeId,
|
||||
NodeLayout,
|
||||
Point,
|
||||
SlotId,
|
||||
SlotLayout
|
||||
} from './layoutTypes'
|
||||
import type { NodeId, NodeLayout, Point } from './layoutTypes'
|
||||
|
||||
/**
|
||||
* Base operation interface that all operations extend
|
||||
@@ -43,10 +37,6 @@ export type OperationType =
|
||||
| 'deleteNode'
|
||||
| 'setNodeVisibility'
|
||||
| 'batchUpdate'
|
||||
| 'createSlot'
|
||||
| 'updateSlot'
|
||||
| 'deleteSlot'
|
||||
| 'batchUpdateSlots'
|
||||
|
||||
/**
|
||||
* Move node operation
|
||||
@@ -109,56 +99,6 @@ export interface BatchUpdateOperation extends BaseOperation {
|
||||
previousValues: Partial<NodeLayout>
|
||||
}
|
||||
|
||||
/**
|
||||
* Base slot operation interface
|
||||
*/
|
||||
export interface BaseSlotOperation {
|
||||
/** Unique operation ID for deduplication */
|
||||
id?: string
|
||||
/** Timestamp for ordering operations */
|
||||
timestamp: number
|
||||
/** Actor who performed the operation (for CRDT) */
|
||||
actor: string
|
||||
/** Source system that initiated the operation */
|
||||
source: 'canvas' | 'vue' | 'external'
|
||||
/** Slot this operation affects */
|
||||
slotId: SlotId
|
||||
}
|
||||
|
||||
/**
|
||||
* Create slot operation
|
||||
*/
|
||||
export interface CreateSlotOperation extends BaseSlotOperation {
|
||||
type: 'createSlot'
|
||||
layout: SlotLayout
|
||||
}
|
||||
|
||||
/**
|
||||
* Update slot position operation
|
||||
*/
|
||||
export interface UpdateSlotOperation extends BaseSlotOperation {
|
||||
type: 'updateSlot'
|
||||
position: Point
|
||||
previousPosition: Point
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete slot operation
|
||||
*/
|
||||
export interface DeleteSlotOperation extends BaseSlotOperation {
|
||||
type: 'deleteSlot'
|
||||
previousLayout: SlotLayout
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update slots operation for a node
|
||||
*/
|
||||
export interface BatchUpdateSlotsOperation extends BaseOperation {
|
||||
type: 'batchUpdateSlots'
|
||||
slots: SlotLayout[]
|
||||
previousSlots: SlotLayout[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Union of all operation types
|
||||
*/
|
||||
@@ -170,10 +110,6 @@ export type LayoutOperation =
|
||||
| DeleteNodeOperation
|
||||
| SetNodeVisibilityOperation
|
||||
| BatchUpdateOperation
|
||||
| CreateSlotOperation
|
||||
| UpdateSlotOperation
|
||||
| DeleteSlotOperation
|
||||
| BatchUpdateSlotsOperation
|
||||
|
||||
/**
|
||||
* Type guards for operations
|
||||
@@ -205,22 +141,6 @@ export const isDeleteNodeOperation = (
|
||||
op: LayoutOperation
|
||||
): op is DeleteNodeOperation => op.type === 'deleteNode'
|
||||
|
||||
export const isCreateSlotOperation = (
|
||||
op: LayoutOperation
|
||||
): op is CreateSlotOperation => op.type === 'createSlot'
|
||||
|
||||
export const isUpdateSlotOperation = (
|
||||
op: LayoutOperation
|
||||
): op is UpdateSlotOperation => op.type === 'updateSlot'
|
||||
|
||||
export const isDeleteSlotOperation = (
|
||||
op: LayoutOperation
|
||||
): op is DeleteSlotOperation => op.type === 'deleteSlot'
|
||||
|
||||
export const isBatchUpdateSlotsOperation = (
|
||||
op: LayoutOperation
|
||||
): op is BatchUpdateSlotsOperation => op.type === 'batchUpdateSlots'
|
||||
|
||||
/**
|
||||
* Operation application interface
|
||||
*/
|
||||
|
||||
@@ -131,23 +131,15 @@ export interface LayoutChange {
|
||||
|
||||
// Store interfaces
|
||||
export interface LayoutStore {
|
||||
// Node accessors
|
||||
// CustomRef accessors for shared write access
|
||||
getNodeLayoutRef(nodeId: NodeId): Ref<NodeLayout | null>
|
||||
getNodesInBounds(bounds: Bounds): ComputedRef<NodeId[]>
|
||||
getAllNodes(): ComputedRef<ReadonlyMap<NodeId, NodeLayout>>
|
||||
|
||||
// Slot accessors
|
||||
getSlotLayoutRef(slotId: SlotId): Ref<SlotLayout | null>
|
||||
getNodeSlots(nodeId: NodeId): ComputedRef<SlotLayout[]>
|
||||
getAllSlots(): ComputedRef<ReadonlyMap<SlotId, SlotLayout>>
|
||||
|
||||
// Version tracking
|
||||
getVersion(): ComputedRef<number>
|
||||
|
||||
// Spatial queries (non-reactive)
|
||||
queryNodeAtPoint(point: Point): NodeId | null
|
||||
queryNodesInBounds(bounds: Bounds): NodeId[]
|
||||
querySlotAtPoint(point: Point): SlotId | null
|
||||
|
||||
// Direct mutation API (CRDT-ready)
|
||||
applyOperation(operation: LayoutOperation): void
|
||||
@@ -171,7 +163,6 @@ export interface LayoutStore {
|
||||
export type {
|
||||
LayoutOperation as AnyLayoutOperation,
|
||||
BaseOperation,
|
||||
BaseSlotOperation,
|
||||
MoveNodeOperation,
|
||||
ResizeNodeOperation,
|
||||
SetNodeZIndexOperation,
|
||||
@@ -179,10 +170,6 @@ export type {
|
||||
DeleteNodeOperation,
|
||||
SetNodeVisibilityOperation,
|
||||
BatchUpdateOperation,
|
||||
CreateSlotOperation,
|
||||
UpdateSlotOperation,
|
||||
DeleteSlotOperation,
|
||||
BatchUpdateSlotsOperation,
|
||||
OperationType,
|
||||
OperationApplicator,
|
||||
OperationSerializer,
|
||||
|
||||
Reference in New Issue
Block a user