mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 02:04:09 +00:00
Cleanup: Litegraph/Vue synchronization work (#5789)
## 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)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<GraphNodeManager | null>(null)
|
||||
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
|
||||
|
||||
// Sync management
|
||||
const slotSync = shallowRef<ReturnType<typeof useSlotLayoutSync> | null>(null)
|
||||
const slotSyncStarted = ref(false)
|
||||
const linkSync = shallowRef<ReturnType<typeof useLinkLayoutSync> | null>(null)
|
||||
|
||||
// Vue node data state
|
||||
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(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,
|
||||
|
||||
@@ -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<HTMLCanvasElement>) => {
|
||||
export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
const workflowService = useWorkflowService()
|
||||
|
||||
@@ -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<HTMLElement | null>,
|
||||
options: Omit<Parameters<typeof dropTargetForElements>[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<HTMLElement | null>,
|
||||
options: Omit<Parameters<typeof draggable>[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(() => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -251,6 +251,8 @@ export interface IBaseWidget<
|
||||
TType extends string = string,
|
||||
TOptions extends IWidgetOptions<unknown> = IWidgetOptions<unknown>
|
||||
> {
|
||||
[symbol: symbol]: boolean
|
||||
|
||||
linkedWidgets?: IBaseWidget[]
|
||||
|
||||
name: string
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<typeof useCanvasStore>['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,
|
||||
|
||||
@@ -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<LGraphCanvas>()
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -157,11 +157,22 @@ export class ComfyApp {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
_nodeOutputs: Record<string, any>
|
||||
nodePreviewImages: Record<string, string[]>
|
||||
// @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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user