diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 467da7e5d..abb0e3666 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -183,6 +183,7 @@ let cleanupNodeManager: (() => void) | null = null // Slot layout sync management let slotSync: ReturnType | null = null +let slotSyncStarted = false let linkSync: ReturnType | null = null const vueNodeData = ref>(new Map()) const nodeState = ref>(new Map()) @@ -240,12 +241,6 @@ const initializeNodeManager = () => { const { startSync } = useLayoutSync() startSync(canvasStore.canvas) - // Initialize slot layout sync for hit detection - slotSync = useSlotLayoutSync() - if (canvasStore.canvas) { - slotSync.start(canvasStore.canvas as LGraphCanvas) - } - // Initialize link layout sync for event-driven updates linkSync = useLinkLayoutSync() if (canvasStore.canvas) { @@ -266,12 +261,6 @@ const disposeNodeManagerAndSyncs = () => { nodeManager = null cleanupNodeManager = null - // Clean up slot layout sync - if (slotSync) { - slotSync.stop() - slotSync = null - } - // Clean up link layout sync if (linkSync) { linkSync.stop() @@ -298,6 +287,68 @@ watch( { immediate: true } ) +// Ensure slot layout sync starts whenever a canvas is available (LiteGraph mode) +watch( + () => canvasStore.canvas, + (canvas, oldCanvas) => { + if (!canvas) { + // Canvas was removed - stop sync if active + if (slotSync && slotSyncStarted) { + slotSync.stop() + slotSyncStarted = false + } + // Clear any stale slot layouts when canvas is torn down + layoutStore.clearAllSlotLayouts() + return + } + + // Canvas changed - restart sync + if (oldCanvas && oldCanvas !== canvas) { + if (slotSync && slotSyncStarted) { + slotSync.stop() + slotSyncStarted = false + } + } + + // Start sync if not in Vue mode and not already started + if (!slotSync) slotSync = useSlotLayoutSync() + if (!slotSyncStarted && !isVueNodesEnabled.value) { + const started = slotSync.start(canvas as LGraphCanvas) + slotSyncStarted = started + } + }, + { immediate: true } +) + +// On rendering mode change, clear slot layouts and manage slot sync +watch( + () => isVueNodesEnabled.value, + (enabled) => { + // Always clear invalid slot layouts from the prior mode + layoutStore.clearAllSlotLayouts() + + if (enabled) { + // Switching TO Vue: Stop slot sync to avoid duplicate registration + if (slotSync && slotSyncStarted) { + slotSync.stop() + slotSyncStarted = false + } + // DOM will re-register via useDomSlotRegistration + } else { + // Switching TO LiteGraph + if (canvasStore.canvas && comfyApp.graph) { + // Ensure slot sync is active + if (!slotSync) slotSync = useSlotLayoutSync() + if (!slotSyncStarted) { + const started = slotSync.start(canvasStore.canvas as LGraphCanvas) + slotSyncStarted = started + } + } + } + }, + { immediate: false } +) + // Transform state for viewport culling const { syncWithCanvas } = useTransformState() @@ -726,6 +777,7 @@ onUnmounted(() => { if (slotSync) { slotSync.stop() slotSync = null + slotSyncStarted = false } if (linkSync) { linkSync.stop() diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index 5be50702b..4cf288512 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -371,7 +371,19 @@ class LayoutStoreImpl implements LayoutStore { updateSlotLayout(key: string, layout: SlotLayout): void { const existing = this.slotLayouts.get(key) - if (!existing) { + if (existing) { + // Short-circuit if bounds and position unchanged (prevents spatial index churn) + if ( + existing.bounds.x === layout.bounds.x && + existing.bounds.y === layout.bounds.y && + existing.bounds.width === layout.bounds.width && + existing.bounds.height === layout.bounds.height && + existing.position.x === layout.position.x && + existing.position.y === layout.position.y + ) { + return + } + } else { logger.debug('Adding slot:', { nodeId: layout.nodeId, type: layout.type, @@ -419,6 +431,15 @@ class LayoutStoreImpl implements LayoutStore { } } + /** + * Clear all slot layouts and their spatial index (O(1) operations) + * Used when switching rendering modes (Vue ↔ LiteGraph) + */ + clearAllSlotLayouts(): void { + this.slotLayouts.clear() + this.slotSpatialIndex.clear() + } + /** * Update reroute layout data */ diff --git a/src/renderer/core/layout/sync/useSlotLayoutSync.ts b/src/renderer/core/layout/sync/useSlotLayoutSync.ts index 2bc7d53f3..a41616411 100644 --- a/src/renderer/core/layout/sync/useSlotLayoutSync.ts +++ b/src/renderer/core/layout/sync/useSlotLayoutSync.ts @@ -16,7 +16,7 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore' * Compute and register slot layouts for a node * @param node LiteGraph node to process */ -function computeAndRegisterSlots(node: LGraphNode): void { +export function computeAndRegisterSlots(node: LGraphNode): void { const nodeId = String(node.id) const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value @@ -59,15 +59,16 @@ export function useSlotLayoutSync() { /** * Start slot layout sync with full event-driven functionality * @param canvas LiteGraph canvas instance + * @returns true if sync was actually started, false if early-returned */ - function start(canvas: LGraphCanvas): void { + function start(canvas: LGraphCanvas): boolean { // When Vue nodes are enabled, slot DOM registers exact positions. // Skip calculated registration to avoid conflicts. if (LiteGraph.vueNodesMode) { - return + return false } const graph = canvas?.graph - if (!graph) return + if (!graph) return false // Initial registration for all nodes in the current graph for (const node of graph._nodes) { @@ -135,6 +136,8 @@ export function useSlotLayoutSync() { graph.onTrigger = origTrigger || undefined graph.onAfterChange = origAfterChange || undefined } + + return true } /** diff --git a/src/renderer/core/layout/types.ts b/src/renderer/core/layout/types.ts index 8ff25ea25..c672f3e94 100644 --- a/src/renderer/core/layout/types.ts +++ b/src/renderer/core/layout/types.ts @@ -297,6 +297,7 @@ export interface LayoutStore { deleteSlotLayout(key: string): void deleteNodeSlotLayouts(nodeId: NodeId): void deleteRerouteLayout(rerouteId: RerouteId): void + clearAllSlotLayouts(): void // Get layout data getLinkLayout(linkId: LinkId): LinkLayout | null