From 840f7f04faa6e0fec59e0b6c92b6f00310514fdf Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Sat, 27 Sep 2025 16:01:59 -0700 Subject: [PATCH] Cleanup: Litegraph/Vue synchronization work (#5789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Cleanup and fixes to the existing syncing logic. ## Review Focus This is probably enough to review and test now. Main things that should still work: - moving nodes around - adding new ones - switching back and forth between Vue and Litegraph Let me know if you find any bugs that weren't already present there. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5789-WIP-Litegraph-Vue-synchronization-work-27a6d73d3650811682cacacb82367b9e) by [Unito](https://www.unito.io) --- .../links/linkInteraction.spec.ts | 6 +- src/components/graph/GraphCanvas.vue | 7 +- src/composables/graph/useVueNodeLifecycle.ts | 111 ++++--------- src/composables/useCanvasDrop.ts | 2 +- src/composables/usePragmaticDragAndDrop.ts | 18 +- src/extensions/core/groupNode.ts | 2 +- src/lib/litegraph/src/LGraphCanvas.ts | 8 +- src/lib/litegraph/src/types/widgets.ts | 2 + src/renderer/core/canvas/canvasStore.ts | 2 +- .../canvas/links/slotLinkCompatibility.ts | 3 +- .../canvas/litegraph/litegraphLinkAdapter.ts | 138 +--------------- .../core/layout/sync/useLayoutSync.ts | 31 ++-- .../core/layout/sync/useLinkLayoutSync.ts | 156 ++++++------------ .../core/layout/sync/useSlotLayoutSync.ts | 45 ++--- src/scripts/app.ts | 29 +++- src/scripts/widgets.ts | 3 - 16 files changed, 148 insertions(+), 415 deletions(-) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index 531f25c7a8..7c0cd4c1dd 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -100,7 +100,7 @@ test.describe('Vue Node Link Interaction', () => { const linkDetails = await comfyPage.page.evaluate((sourceId) => { const app = window['app'] - const graph = app?.canvas?.graph ?? app?.graph + const graph = app?.canvas?.graph if (!graph) return null const source = graph.getNodeById(sourceId) @@ -164,7 +164,7 @@ test.describe('Vue Node Link Interaction', () => { const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { const app = window['app'] - const graph = app?.canvas?.graph ?? app?.graph + const graph = app?.canvas?.graph if (!graph) return 0 const source = graph.getNodeById(sourceId) @@ -207,7 +207,7 @@ test.describe('Vue Node Link Interaction', () => { const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { const app = window['app'] - const graph = app?.canvas?.graph ?? app?.graph + const graph = app?.canvas?.graph if (!graph) return 0 const source = graph.getNodeById(sourceId) diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 260f4b9b65..4e19e7d896 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -93,6 +93,7 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu import SideToolbar from '@/components/sidebar/SideToolbar.vue' import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue' import { useChainCallback } from '@/composables/functional/useChainCallback' +import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { useViewportCulling } from '@/composables/graph/useViewportCulling' import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import { useNodeBadge } from '@/composables/node/useNodeBadge' @@ -189,8 +190,8 @@ watch( } ) -const allNodes = computed(() => - Array.from(vueNodeLifecycle.vueNodeData.value.values()) +const allNodes = computed((): VueNodeData[] => + Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? []) ) watchEffect(() => { @@ -225,7 +226,6 @@ watch( for (const n of comfyApp.graph.nodes) { if (!n.widgets) continue for (const w of n.widgets) { - // @ts-expect-error fixme ts strict error if (w[IS_CONTROL_WIDGET]) { updateControlWidgetLabel(w) if (w.linkedWidgets) { @@ -364,7 +364,6 @@ const loadCustomNodesI18n = async () => { const comfyAppReady = ref(false) const workflowPersistence = useWorkflowPersistence() -// @ts-expect-error fixme ts strict error useCanvasDrop(canvasRef) useLitegraphSettings() useNodeBadge() diff --git a/src/composables/graph/useVueNodeLifecycle.ts b/src/composables/graph/useVueNodeLifecycle.ts index 84e095b5fc..ff89dfc855 100644 --- a/src/composables/graph/useVueNodeLifecycle.ts +++ b/src/composables/graph/useVueNodeLifecycle.ts @@ -1,11 +1,8 @@ import { createSharedComposable } from '@vueuse/core' -import { readonly, ref, shallowRef, watch } from 'vue' +import { shallowRef, watch } from 'vue' import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager' -import type { - GraphNodeManager, - VueNodeData -} from '@/composables/graph/useGraphNodeManager' +import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager' import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags' import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' @@ -22,31 +19,19 @@ function useVueNodeLifecycleIndividual() { const { shouldRenderVueNodes } = useVueFeatureFlags() const nodeManager = shallowRef(null) - const cleanupNodeManager = shallowRef<(() => void) | null>(null) - // Sync management - const slotSync = shallowRef | null>(null) - const slotSyncStarted = ref(false) - const linkSync = shallowRef | null>(null) - - // Vue node data state - const vueNodeData = ref>(new Map()) - - // Trigger for forcing computed re-evaluation - const nodeDataTrigger = ref(0) + const { startSync } = useLayoutSync() + const linkSyncManager = useLinkLayoutSync() + const slotSyncManager = useSlotLayoutSync() const initializeNodeManager = () => { // Use canvas graph if available (handles subgraph contexts), fallback to app graph - const activeGraph = comfyApp.canvas?.graph || comfyApp.graph + const activeGraph = comfyApp.canvas?.graph if (!activeGraph || nodeManager.value) return // Initialize the core node manager const manager = useGraphNodeManager(activeGraph) nodeManager.value = manager - cleanupNodeManager.value = manager.cleanup - - // Use the manager's data maps - vueNodeData.value = manager.vueNodeData // Initialize layout system with existing nodes from active graph const nodes = activeGraph._nodes.map((node: LGraphNode) => ({ @@ -76,46 +61,29 @@ function useVueNodeLifecycleIndividual() { } // Initialize layout sync (one-way: Layout Store → LiteGraph) - const { startSync } = useLayoutSync() startSync(canvasStore.canvas) - // Initialize link layout sync for event-driven updates - const linkSyncManager = useLinkLayoutSync() - linkSync.value = linkSyncManager if (comfyApp.canvas) { linkSyncManager.start(comfyApp.canvas) } - - // Force computed properties to re-evaluate - nodeDataTrigger.value++ } const disposeNodeManagerAndSyncs = () => { if (!nodeManager.value) return try { - cleanupNodeManager.value?.() + nodeManager.value.cleanup() } catch { /* empty */ } nodeManager.value = null - cleanupNodeManager.value = null - // Clean up link layout sync - if (linkSync.value) { - linkSync.value.stop() - linkSync.value = null - } - - // Reset reactive maps to clean state - vueNodeData.value = new Map() + linkSyncManager.stop() } // Watch for Vue nodes enabled state changes watch( - () => - shouldRenderVueNodes.value && - Boolean(comfyApp.canvas?.graph || comfyApp.graph), + () => shouldRenderVueNodes.value && Boolean(comfyApp.canvas?.graph), (enabled) => { if (enabled) { initializeNodeManager() @@ -138,20 +106,14 @@ function useVueNodeLifecycleIndividual() { } // Switching to Vue - if (vueMode && slotSyncStarted.value) { - slotSync.value?.stop() - slotSyncStarted.value = false + if (vueMode) { + slotSyncManager.stop() } // Switching to LG const shouldRun = Boolean(canvas?.graph) && !vueMode - if (shouldRun && !slotSyncStarted.value && canvas) { - // Initialize slot sync if not already created - if (!slotSync.value) { - slotSync.value = useSlotLayoutSync() - } - const started = slotSync.value.attemptStart(canvas as LGraphCanvas) - slotSyncStarted.value = started + if (shouldRun && canvas) { + slotSyncManager.attemptStart(canvas as LGraphCanvas) } }, { immediate: true } @@ -159,26 +121,27 @@ function useVueNodeLifecycleIndividual() { // Handle case where Vue nodes are enabled but graph starts empty const setupEmptyGraphListener = () => { + const activeGraph = comfyApp.canvas?.graph if ( - shouldRenderVueNodes.value && - comfyApp.graph && - !nodeManager.value && - comfyApp.graph._nodes.length === 0 + !shouldRenderVueNodes.value || + nodeManager.value || + activeGraph?._nodes.length !== 0 ) { - const originalOnNodeAdded = comfyApp.graph.onNodeAdded - comfyApp.graph.onNodeAdded = function (node: LGraphNode) { - // Restore original handler - comfyApp.graph.onNodeAdded = originalOnNodeAdded + return + } + const originalOnNodeAdded = activeGraph.onNodeAdded + activeGraph.onNodeAdded = function (node: LGraphNode) { + // Restore original handler + activeGraph.onNodeAdded = originalOnNodeAdded - // Initialize node manager if needed - if (shouldRenderVueNodes.value && !nodeManager.value) { - initializeNodeManager() - } + // Initialize node manager if needed + if (shouldRenderVueNodes.value && !nodeManager.value) { + initializeNodeManager() + } - // Call original handler - if (originalOnNodeAdded) { - originalOnNodeAdded.call(this, node) - } + // Call original handler + if (originalOnNodeAdded) { + originalOnNodeAdded.call(this, node) } } } @@ -189,20 +152,12 @@ function useVueNodeLifecycleIndividual() { nodeManager.value.cleanup() nodeManager.value = null } - if (slotSyncStarted.value) { - slotSync.value?.stop() - slotSyncStarted.value = false - } - slotSync.value = null - if (linkSync.value) { - linkSync.value.stop() - linkSync.value = null - } + slotSyncManager.stop() + linkSyncManager.stop() } return { - vueNodeData, - nodeManager: readonly(nodeManager), + nodeManager, // Lifecycle methods initializeNodeManager, diff --git a/src/composables/useCanvasDrop.ts b/src/composables/useCanvasDrop.ts index 76db7e5135..d3870b04b7 100644 --- a/src/composables/useCanvasDrop.ts +++ b/src/composables/useCanvasDrop.ts @@ -14,7 +14,7 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore' import { ComfyNodeDefImpl } from '@/stores/nodeDefStore' import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes' -export const useCanvasDrop = (canvasRef: Ref) => { +export const useCanvasDrop = (canvasRef: Ref) => { const modelToNodeStore = useModelToNodeStore() const litegraphService = useLitegraphService() const workflowService = useWorkflowService() diff --git a/src/composables/usePragmaticDragAndDrop.ts b/src/composables/usePragmaticDragAndDrop.ts index 642b9e310a..ed000b72e6 100644 --- a/src/composables/usePragmaticDragAndDrop.ts +++ b/src/composables/usePragmaticDragAndDrop.ts @@ -2,19 +2,17 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter' -import { onBeforeUnmount, onMounted } from 'vue' +import { toValue } from 'vue' +import { type MaybeRefOrGetter, onBeforeUnmount, onMounted } from 'vue' export function usePragmaticDroppable( - dropTargetElement: HTMLElement | (() => HTMLElement), + dropTargetElement: MaybeRefOrGetter, options: Omit[0], 'element'> ) { let cleanup = () => {} onMounted(() => { - const element = - typeof dropTargetElement === 'function' - ? dropTargetElement() - : dropTargetElement + const element = toValue(dropTargetElement) if (!element) { return @@ -32,16 +30,13 @@ export function usePragmaticDroppable( } export function usePragmaticDraggable( - draggableElement: HTMLElement | (() => HTMLElement), + draggableElement: MaybeRefOrGetter, options: Omit[0], 'element'> ) { let cleanup = () => {} onMounted(() => { - const element = - typeof draggableElement === 'function' - ? draggableElement() - : draggableElement + const element = toValue(draggableElement) if (!element) { return @@ -51,6 +46,7 @@ export function usePragmaticDraggable( element, ...options }) + // TODO: Change to onScopeDispose }) onBeforeUnmount(() => { diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index 4b3ce78bca..df063ed0b0 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -170,7 +170,7 @@ class GroupNodeBuilder { // Use the built in copyToClipboard function to generate the node data we need try { // @ts-expect-error fixme ts strict error - const serialised = serialise(this.nodes, app.canvas.graph) + const serialised = serialise(this.nodes, app.canvas?.graph) const config = JSON.parse(serialised) storeLinkTypes(config) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 2c952787dd..22b71c66a3 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -757,9 +757,7 @@ export class LGraphCanvas // Initialize link renderer if graph is available if (graph) { - this.linkRenderer = new LitegraphLinkAdapter(graph) - // Disable layout writes during render - this.linkRenderer.enableLayoutStoreWrites = false + this.linkRenderer = new LitegraphLinkAdapter(false) } this.linkConnector.events.addEventListener('link-created', () => @@ -1858,9 +1856,7 @@ export class LGraphCanvas newGraph.attachCanvas(this) // Re-initialize link renderer with new graph - this.linkRenderer = new LitegraphLinkAdapter(newGraph) - // Disable layout writes during render - this.linkRenderer.enableLayoutStoreWrites = false + this.linkRenderer = new LitegraphLinkAdapter(false) this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph }) this.#dirty() diff --git a/src/lib/litegraph/src/types/widgets.ts b/src/lib/litegraph/src/types/widgets.ts index 850fe9bcb1..1b01b66b0a 100644 --- a/src/lib/litegraph/src/types/widgets.ts +++ b/src/lib/litegraph/src/types/widgets.ts @@ -251,6 +251,8 @@ export interface IBaseWidget< TType extends string = string, TOptions extends IWidgetOptions = IWidgetOptions > { + [symbol: symbol]: boolean + linkedWidgets?: IBaseWidget[] name: string diff --git a/src/renderer/core/canvas/canvasStore.ts b/src/renderer/core/canvas/canvasStore.ts index ec38940fe9..4c55392049 100644 --- a/src/renderer/core/canvas/canvasStore.ts +++ b/src/renderer/core/canvas/canvasStore.ts @@ -116,7 +116,7 @@ export const useCanvasStore = defineStore('canvas', () => { newCanvas.canvas, 'litegraph:set-graph', (event: CustomEvent<{ newGraph: LGraph; oldGraph: LGraph }>) => { - const newGraph = event.detail?.newGraph || app.canvas?.graph + const newGraph = event.detail?.newGraph ?? app.canvas?.graph // TODO: Ambiguous Graph currentGraph.value = newGraph isInSubgraph.value = Boolean(app.canvas?.subgraph) } diff --git a/src/renderer/core/canvas/links/slotLinkCompatibility.ts b/src/renderer/core/canvas/links/slotLinkCompatibility.ts index b8beffc384..dd00e8c90a 100644 --- a/src/renderer/core/canvas/links/slotLinkCompatibility.ts +++ b/src/renderer/core/canvas/links/slotLinkCompatibility.ts @@ -10,7 +10,6 @@ import type { SlotDragSource, SlotDropCandidate } from '@/renderer/core/canvas/links/slotLinkDragState' -import { app } from '@/scripts/app' interface CompatibilityResult { allowable: boolean @@ -21,7 +20,7 @@ interface CompatibilityResult { function resolveNode(nodeId: NodeId) { const pinia = getActivePinia() const canvasStore = pinia ? useCanvasStore() : null - const graph = canvasStore?.canvas?.graph ?? app.canvas?.graph + const graph = canvasStore?.canvas?.graph if (!graph) return null const id = typeof nodeId === 'string' ? Number(nodeId) : nodeId if (Number.isNaN(id)) return null diff --git a/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts b/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts index 1bb3f7dae5..da2e57b884 100644 --- a/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts +++ b/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts @@ -6,7 +6,6 @@ * rendering data that can be consumed by the PathRenderer. * Maintains backward compatibility with existing litegraph integration. */ -import type { LGraph } from '@/lib/litegraph/src/LGraph' import type { LLink } from '@/lib/litegraph/src/LLink' import type { Reroute } from '@/lib/litegraph/src/Reroute' import type { @@ -19,7 +18,6 @@ import { LinkMarkerShape, LinkRenderType } from '@/lib/litegraph/src/types/globalEnums' -import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' import { type ArrowShape, CanvasPathRenderer, @@ -54,142 +52,10 @@ export interface LinkRenderContext { disabledPattern?: CanvasPattern | null } -interface LinkRenderOptions { - color?: CanvasColour - flow?: boolean - skipBorder?: boolean - disabled?: boolean -} - export class LitegraphLinkAdapter { - private graph: LGraph - private pathRenderer: CanvasPathRenderer - public enableLayoutStoreWrites = true + private readonly pathRenderer = new CanvasPathRenderer() - constructor(graph: LGraph) { - this.graph = graph - this.pathRenderer = new CanvasPathRenderer() - } - - /** - * Render a single link with all necessary data properly fetched - * Populates link.path for hit detection - */ - renderLink( - ctx: CanvasRenderingContext2D, - link: LLink, - context: LinkRenderContext, - options: LinkRenderOptions = {} - ): void { - // Get nodes from graph - const sourceNode = this.graph.getNodeById(link.origin_id) - const targetNode = this.graph.getNodeById(link.target_id) - - if (!sourceNode || !targetNode) { - console.warn(`Cannot render link ${link.id}: missing nodes`) - return - } - - // Get slots from nodes - const sourceSlot = sourceNode.outputs?.[link.origin_slot] - const targetSlot = targetNode.inputs?.[link.target_slot] - - if (!sourceSlot || !targetSlot) { - console.warn(`Cannot render link ${link.id}: missing slots`) - return - } - - // Get positions using layout tree data if available - const startPos = getSlotPosition( - sourceNode, - link.origin_slot, - false // output - ) - const endPos = getSlotPosition( - targetNode, - link.target_slot, - true // input - ) - - // Get directions from slots - const startDir = sourceSlot.dir || LinkDirection.RIGHT - const endDir = targetSlot.dir || LinkDirection.LEFT - - // Convert to pure render data - const linkData = this.convertToLinkRenderData( - link, - { x: startPos[0], y: startPos[1] }, - { x: endPos[0], y: endPos[1] }, - startDir, - endDir, - options - ) - - // Convert context - const pathContext = this.convertToPathRenderContext(context) - - // Render using pure renderer - const path = this.pathRenderer.drawLink(ctx, linkData, pathContext) - - // Store path for hit detection - link.path = path - - // Update layout store when writes are enabled (event-driven path) - if (this.enableLayoutStoreWrites && link.id !== -1) { - // Calculate bounds and center only when writing - const bounds = this.calculateLinkBounds(startPos, endPos, linkData) - const centerPos = linkData.centerPos || { - x: (startPos[0] + endPos[0]) / 2, - y: (startPos[1] + endPos[1]) / 2 - } - - layoutStore.updateLinkLayout(link.id, { - id: link.id, - path: path, - bounds: bounds, - centerPos: centerPos, - sourceNodeId: String(link.origin_id), - targetNodeId: String(link.target_id), - sourceSlot: link.origin_slot, - targetSlot: link.target_slot - }) - - // Also update segment layout for the whole link (null rerouteId means final segment) - layoutStore.updateLinkSegmentLayout(link.id, null, { - path: path, - bounds: bounds, - centerPos: centerPos - }) - } - } - - /** - * Convert litegraph link data to pure render format - */ - private convertToLinkRenderData( - link: LLink, - startPoint: Point, - endPoint: Point, - startDir: LinkDirection, - endDir: LinkDirection, - options: LinkRenderOptions - ): LinkRenderData { - return { - id: String(link.id), - startPoint, - endPoint, - startDirection: this.convertDirection(startDir), - endDirection: this.convertDirection(endDir), - color: options.color - ? String(options.color) - : link.color - ? String(link.color) - : undefined, - type: link.type !== undefined ? String(link.type) : undefined, - flow: options.flow || false, - disabled: options.disabled || false - } - } + constructor(public readonly enableLayoutStoreWrites = true) {} /** * Convert LinkDirection enum to Direction string diff --git a/src/renderer/core/layout/sync/useLayoutSync.ts b/src/renderer/core/layout/sync/useLayoutSync.ts index 2aee7974cf..cdecabd492 100644 --- a/src/renderer/core/layout/sync/useLayoutSync.ts +++ b/src/renderer/core/layout/sync/useLayoutSync.ts @@ -5,7 +5,9 @@ * The layout store is the single source of truth. */ import { onUnmounted } from 'vue' +import { ref } from 'vue' +import type { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' /** @@ -13,27 +15,27 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore' * This replaces the bidirectional sync with a one-way sync */ export function useLayoutSync() { - let unsubscribe: (() => void) | null = null + const unsubscribe = ref<() => void>() /** - * Start syncing from Layout system to LiteGraph - * This is one-way: Layout → LiteGraph only + * Start syncing from Layout → LiteGraph */ - function startSync(canvas: any) { + function startSync(canvas: ReturnType['canvas']) { if (!canvas?.graph) return + // Cancel last subscription + stopSync() // Subscribe to layout changes - unsubscribe = layoutStore.onChange((change) => { + unsubscribe.value = layoutStore.onChange((change) => { // Apply changes to LiteGraph regardless of source // The layout store is the single source of truth for (const nodeId of change.nodeIds) { const layout = layoutStore.getNodeLayoutRef(nodeId).value if (!layout) continue - const liteNode = canvas.graph.getNodeById(parseInt(nodeId)) + const liteNode = canvas.graph?.getNodeById(parseInt(nodeId)) if (!liteNode) continue - // Update position if changed if ( liteNode.pos[0] !== layout.position.x || liteNode.pos[1] !== layout.position.y @@ -42,7 +44,6 @@ export function useLayoutSync() { liteNode.pos[1] = layout.position.y } - // Update size if changed if ( liteNode.size[0] !== layout.size.width || liteNode.size[1] !== layout.size.height @@ -57,20 +58,12 @@ export function useLayoutSync() { }) } - /** - * Stop syncing - */ function stopSync() { - if (unsubscribe) { - unsubscribe() - unsubscribe = null - } + unsubscribe.value?.() + unsubscribe.value = undefined } - // Auto-cleanup on unmount - onUnmounted(() => { - stopSync() - }) + onUnmounted(stopSync) return { startSync, diff --git a/src/renderer/core/layout/sync/useLinkLayoutSync.ts b/src/renderer/core/layout/sync/useLinkLayoutSync.ts index fd6b3b19c3..b39f97b451 100644 --- a/src/renderer/core/layout/sync/useLinkLayoutSync.ts +++ b/src/renderer/core/layout/sync/useLinkLayoutSync.ts @@ -1,14 +1,6 @@ -/** - * Composable for event-driven link layout synchronization - * - * Implements event-driven link layout updates decoupled from the render cycle. - * Updates link geometry only when it actually changes (node move/resize, link create/delete, - * reroute create/delete/move, collapse toggles). - */ -import log from 'loglevel' -import { onUnmounted } from 'vue' +import { tryOnScopeDispose } from '@vueuse/core' +import { computed, ref, toValue } from 'vue' -import type { LGraph } from '@/lib/litegraph/src/LGraph' import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' import { LLink } from '@/lib/litegraph/src/LLink' import { Reroute } from '@/lib/litegraph/src/Reroute' @@ -20,23 +12,17 @@ import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculatio import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import type { LayoutChange } from '@/renderer/core/layout/types' -const logger = log.getLogger('useLinkLayoutSync') - -/** - * Composable for managing link layout synchronization - */ export function useLinkLayoutSync() { - let canvas: LGraphCanvas | null = null - let graph: LGraph | null = null - let offscreenCtx: CanvasRenderingContext2D | null = null - let adapter: LitegraphLinkAdapter | null = null - let unsubscribeLayoutChange: (() => void) | null = null - let restoreHandlers: (() => void) | null = null + const canvasRef = ref() + const graphRef = computed(() => canvasRef.value?.graph) + const unsubscribeLayoutChange = ref<() => void>() + const adapter = new LitegraphLinkAdapter() /** * Build link render context from canvas properties */ function buildLinkRenderContext(): LinkRenderContext { + const canvas = toValue(canvasRef) if (!canvas) { throw new Error('Canvas not initialized') } @@ -73,7 +59,9 @@ export function useLinkLayoutSync() { * - No dragging state handling (pure geometry computation) */ function recomputeLinkById(linkId: number): void { - if (!graph || !adapter || !offscreenCtx || !canvas) return + const canvas = toValue(canvasRef) + const graph = toValue(graphRef) + if (!graph || !canvas) return const link = graph.links.get(linkId) if (!link || link.id === -1) return // Skip floating/temp links @@ -131,7 +119,7 @@ export function useLinkLayoutSync() { // Render segment to this reroute adapter.renderLinkDirect( - offscreenCtx, + canvas.ctx, segmentStartPos, reroute.pos, link, @@ -167,7 +155,7 @@ export function useLinkLayoutSync() { ] adapter.renderLinkDirect( - offscreenCtx, + canvas.ctx, lastReroute.pos, endPos, link, @@ -185,7 +173,7 @@ export function useLinkLayoutSync() { } else { // No reroutes - render direct link adapter.renderLinkDirect( - offscreenCtx, + canvas.ctx, startPos, endPos, link, @@ -206,6 +194,7 @@ export function useLinkLayoutSync() { * Recompute all links connected to a node */ function recomputeLinksForNode(nodeId: number): void { + const graph = toValue(graphRef) if (!graph) return const node = graph.getNodeById(nodeId) @@ -243,6 +232,7 @@ export function useLinkLayoutSync() { * Recompute all links associated with a reroute */ function recomputeLinksForReroute(rerouteId: number): void { + const graph = toValue(graphRef) if (!graph) return const reroute = graph.reroutes.get(rerouteId) @@ -258,105 +248,55 @@ export function useLinkLayoutSync() { * Start link layout sync with event-driven functionality */ function start(canvasInstance: LGraphCanvas): void { - canvas = canvasInstance - graph = canvas.graph - if (!graph) return - - // Create offscreen canvas context - const offscreenCanvas = document.createElement('canvas') - offscreenCtx = offscreenCanvas.getContext('2d') - if (!offscreenCtx) { - logger.error('Failed to create offscreen canvas context') - return - } - - // Create dedicated adapter with layout writes enabled - adapter = new LitegraphLinkAdapter(graph) - adapter.enableLayoutStoreWrites = true + canvasRef.value = canvasInstance + if (!canvasInstance.graph) return // Initial computation for all existing links - for (const link of graph._links.values()) { + for (const link of canvasInstance.graph._links.values()) { if (link.id !== -1) { recomputeLinkById(link.id) } } // Subscribe to layout store changes - unsubscribeLayoutChange = layoutStore.onChange((change: LayoutChange) => { - switch (change.operation.type) { - case 'moveNode': - case 'resizeNode': - recomputeLinksForNode(parseInt(change.operation.nodeId)) - break - case 'createLink': - recomputeLinkById(change.operation.linkId) - break - case 'deleteLink': - // No-op - store already cleaned by existing code - break - case 'createReroute': - case 'deleteReroute': - // Recompute all affected links - if ('linkIds' in change.operation) { - for (const linkId of change.operation.linkIds) { - recomputeLinkById(linkId) + unsubscribeLayoutChange.value?.() + unsubscribeLayoutChange.value = layoutStore.onChange( + (change: LayoutChange) => { + switch (change.operation.type) { + case 'moveNode': + case 'resizeNode': + recomputeLinksForNode(parseInt(change.operation.nodeId)) + break + case 'createLink': + recomputeLinkById(change.operation.linkId) + break + case 'deleteLink': + // No-op - store already cleaned by existing code + break + case 'createReroute': + case 'deleteReroute': + // Recompute all affected links + if ('linkIds' in change.operation) { + for (const linkId of change.operation.linkIds) { + recomputeLinkById(linkId) + } } - } - break - case 'moveReroute': - recomputeLinksForReroute(change.operation.rerouteId) - break - } - }) - - // Hook collapse events - const origTrigger = graph.onTrigger - - graph.onTrigger = (action: string, param: any) => { - if ( - action === 'node:property:changed' && - param?.property === 'flags.collapsed' - ) { - const nodeId = parseInt(String(param.nodeId)) - if (!isNaN(nodeId)) { - recomputeLinksForNode(nodeId) + break + case 'moveReroute': + recomputeLinksForReroute(change.operation.rerouteId) + break } } - if (origTrigger) { - origTrigger.call(graph, action, param) - } - } - - // Store cleanup function - restoreHandlers = () => { - if (graph) { - graph.onTrigger = origTrigger || undefined - } - } + ) } - /** - * Stop link layout sync and cleanup all resources - */ function stop(): void { - if (unsubscribeLayoutChange) { - unsubscribeLayoutChange() - unsubscribeLayoutChange = null - } - if (restoreHandlers) { - restoreHandlers() - restoreHandlers = null - } - canvas = null - graph = null - offscreenCtx = null - adapter = null + unsubscribeLayoutChange.value?.() + unsubscribeLayoutChange.value = undefined + canvasRef.value = undefined } - // Auto-cleanup on unmount - onUnmounted(() => { - stop() - }) + tryOnScopeDispose(stop) return { start, diff --git a/src/renderer/core/layout/sync/useSlotLayoutSync.ts b/src/renderer/core/layout/sync/useSlotLayoutSync.ts index 281199e8be..099f2497da 100644 --- a/src/renderer/core/layout/sync/useSlotLayoutSync.ts +++ b/src/renderer/core/layout/sync/useSlotLayoutSync.ts @@ -1,10 +1,5 @@ -/** - * Composable for managing slot layout registration - * - * Implements event-driven slot registration decoupled from the draw cycle. - * Registers slots once on initial load and keeps them updated when necessary. - */ -import { onUnmounted } from 'vue' +import { tryOnScopeDispose } from '@vueuse/core' +import { ref } from 'vue' import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' @@ -13,10 +8,6 @@ import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotC import { registerNodeSlots } from '@/renderer/core/layout/slots/register' 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 { const nodeId = String(node.id) const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value @@ -50,12 +41,9 @@ function computeAndRegisterSlots(node: LGraphNode): void { registerNodeSlots(nodeId, context) } -/** - * Composable for managing slot layout registration - */ export function useSlotLayoutSync() { - let unsubscribeLayoutChange: (() => void) | null = null - let restoreHandlers: (() => void) | null = null + const unsubscribeLayoutChange = ref<() => void>() + const restoreHandlers = ref<() => void>() /** * Attempt to start slot layout sync with full event-driven functionality @@ -77,7 +65,8 @@ export function useSlotLayoutSync() { } // Layout changes → recompute slots for changed nodes - unsubscribeLayoutChange = layoutStore.onChange((change) => { + unsubscribeLayoutChange.value?.() + unsubscribeLayoutChange.value = layoutStore.onChange((change) => { for (const nodeId of change.nodeIds) { const node = graph.getNodeById(parseInt(nodeId)) if (node) { @@ -131,7 +120,7 @@ export function useSlotLayoutSync() { } // Store cleanup function - restoreHandlers = () => { + restoreHandlers.value = () => { graph.onNodeAdded = origNodeAdded || undefined graph.onNodeRemoved = origNodeRemoved || undefined // Only restore onTrigger if Vue nodes are not active @@ -145,24 +134,14 @@ export function useSlotLayoutSync() { return true } - /** - * Stop slot layout sync and cleanup all subscriptions - */ function stop(): void { - if (unsubscribeLayoutChange) { - unsubscribeLayoutChange() - unsubscribeLayoutChange = null - } - if (restoreHandlers) { - restoreHandlers() - restoreHandlers = null - } + unsubscribeLayoutChange.value?.() + unsubscribeLayoutChange.value = undefined + restoreHandlers.value?.() + restoreHandlers.value = undefined } - // Auto-cleanup on unmount - onUnmounted(() => { - stop() - }) + tryOnScopeDispose(stop) return { attemptStart, diff --git a/src/scripts/app.ts b/src/scripts/app.ts index d7583724aa..49423436d3 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -157,11 +157,22 @@ export class ComfyApp { // @ts-expect-error fixme ts strict error _nodeOutputs: Record nodePreviewImages: Record - // @ts-expect-error fixme ts strict error - #graph: LGraph + + private rootGraphInternal: LGraph | undefined + + // TODO: Migrate internal usage to the + /** @deprecated Use {@link rootGraph} instead */ get graph() { - return this.#graph + return this.rootGraphInternal! } + + get rootGraph(): LGraph | undefined { + if (!this.rootGraphInternal) { + console.error('ComfyApp graph accessed before initialization') + } + return this.rootGraphInternal + } + // @ts-expect-error fixme ts strict error canvas: LGraphCanvas dragOverNode: LGraphNode | null = null @@ -765,8 +776,7 @@ export class ComfyApp { } } - #addAfterConfigureHandler() { - const { graph } = this + private addAfterConfigureHandler(graph: LGraph) { const { onConfigure } = graph graph.onConfigure = function (...args) { fixLinkInputSlots(this) @@ -809,10 +819,10 @@ export class ComfyApp { this.#addConfigureHandler() this.#addApiUpdateHandlers() - this.#graph = new LGraph() + const graph = new LGraph() // Register the subgraph - adds type wrapper for Litegraph's `createNode` factory - this.graph.events.addEventListener('subgraph-created', (e) => { + graph.events.addEventListener('subgraph-created', (e) => { try { const { subgraph, data } = e.detail useSubgraphService().registerNewSubgraph(subgraph, data) @@ -826,9 +836,10 @@ export class ComfyApp { } }) - this.#addAfterConfigureHandler() + this.addAfterConfigureHandler(graph) - this.canvas = new LGraphCanvas(canvasEl, this.graph) + this.rootGraphInternal = graph + this.canvas = new LGraphCanvas(canvasEl, graph) // Make canvas states reactive so we can observe changes on them. this.canvas.state = reactive(this.canvas.state) diff --git a/src/scripts/widgets.ts b/src/scripts/widgets.ts index 33b20988b5..d87f650dd2 100644 --- a/src/scripts/widgets.ts +++ b/src/scripts/widgets.ts @@ -140,7 +140,6 @@ export function addValueControlWidgets( valueControl.tooltip = 'Allows the linked widget to be changed automatically, for example randomizing the noise seed.' - // @ts-ignore index with symbol valueControl[IS_CONTROL_WIDGET] = true updateControlWidgetLabel(valueControl) const widgets: [IComboWidget, ...IStringWidget[]] = [valueControl] @@ -273,12 +272,10 @@ export function addValueControlWidgets( valueControl.beforeQueued = () => { if (controlValueRunBefore()) { // Don't run on first execution - // @ts-ignore index with symbol if (valueControl[HAS_EXECUTED]) { applyWidgetControl() } } - // @ts-ignore index with symbol valueControl[HAS_EXECUTED] = true }