diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index e2d31755ad..39db42bb51 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -139,8 +139,8 @@ import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence' import { CORE_SETTINGS } from '@/constants/coreSettings' import { i18n, t } from '@/i18n' import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +import { useLayoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' import { layoutStore } from '@/renderer/core/layout/store/LayoutStore' -import { useLayout } from '@/renderer/core/layout/sync/useLayout' import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync' import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync' import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync' @@ -177,7 +177,7 @@ const workspaceStore = useWorkspaceStore() const canvasStore = useCanvasStore() const executionStore = useExecutionStore() const toastStore = useToastStore() -const { mutations: layoutMutations } = useLayout() +const layoutMutations = useLayoutMutations() const betaMenuEnabled = computed( () => settingStore.get('Comfy.UseNewMenu') !== 'Disabled' ) diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index a631a80e45..a651c1436e 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -4,7 +4,7 @@ */ import { nextTick, reactive, readonly } from 'vue' -import { layoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' +import { useLayoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' import type { WidgetValue } from '@/types/simplifiedWidget' import type { SpatialIndexDebugInfo } from '@/types/spatialIndex' @@ -99,6 +99,9 @@ export interface GraphNodeManager { } export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { + // Get layout mutations composable + const { moveNode, resizeNode, createNode, deleteNode, setSource } = + useLayoutMutations() // Safe reactive data extracted from LiteGraph nodes const vueNodeData = reactive(new Map()) const nodeState = reactive(new Map()) @@ -487,7 +490,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { // Push position change to layout store // Source is already set to 'canvas' in detectChangesInRAF - void layoutMutations.moveNode(id, { x: node.pos[0], y: node.pos[1] }) + void moveNode(id, { x: node.pos[0], y: node.pos[1] }) return true } @@ -509,7 +512,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { // Push size change to layout store // Source is already set to 'canvas' in detectChangesInRAF - void layoutMutations.resizeNode(id, { + void resizeNode(id, { width: node.size[0], height: node.size[1] }) @@ -554,7 +557,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { } /** - * Main RAF change detection function - now simplified with extracted helpers + * Main RAF change detection function */ const detectChangesInRAF = () => { const startTime = performance.now() @@ -565,7 +568,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { let sizeUpdates = 0 // Set source for all canvas-driven updates - layoutMutations.setSource(LayoutSource.Canvas) + setSource(LayoutSource.Canvas) // Process each node for changes for (const node of graph._nodes) { @@ -625,8 +628,8 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { spatialIndex.insert(id, bounds, id) // Add node to layout store - layoutMutations.setSource(LayoutSource.Canvas) - void layoutMutations.createNode(id, { + setSource(LayoutSource.Canvas) + void createNode(id, { position: { x: node.pos[0], y: node.pos[1] }, size: { width: node.size[0], height: node.size[1] }, zIndex: node.order || 0, @@ -652,8 +655,8 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { spatialIndex.remove(id) // Remove node from layout store - layoutMutations.setSource(LayoutSource.Canvas) - void layoutMutations.deleteNode(id) + setSource(LayoutSource.Canvas) + void deleteNode(id) // Clean up all tracking references nodeRefs.delete(id) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 350794357a..d35c476d72 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -6,7 +6,8 @@ import { } from '@/lib/litegraph/src/constants' import type { UUID } from '@/lib/litegraph/src/utils/uuid' import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid' -import { layoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' +import { useLayoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' +import type { LayoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' import type { DragAndScaleState } from './DragAndScale' @@ -190,6 +191,9 @@ export class LGraph nodes_executedAction: string[] = [] extra: LGraphExtra = {} + /** Layout mutations instance for this graph */ + layoutMutations!: LayoutMutations + /** @deprecated Deserialising a workflow sets this unused property. */ version?: number @@ -276,6 +280,9 @@ export class LGraph constructor(o?: ISerialisedGraph | SerialisableGraph) { if (LiteGraph.debug) console.log('Graph created') + // Get layout mutations composable + this.layoutMutations = useLayoutMutations() + /** @see MapProxyHandler */ const links = this._links MapProxyHandler.bindAllMethods(links) @@ -1353,8 +1360,8 @@ export class LGraph this.reroutes.set(rerouteId, reroute) // Register reroute in Layout Store for spatial tracking - layoutMutations.setSource(LayoutSource.Canvas) - layoutMutations.createReroute( + this.layoutMutations.setSource(LayoutSource.Canvas) + this.layoutMutations.createReroute( String(rerouteId), { x: pos[0], y: pos[1] }, before.parentId ? String(before.parentId) : undefined, @@ -1436,8 +1443,8 @@ export class LGraph reroutes.delete(id) // Delete reroute from Layout Store - layoutMutations.setSource(LayoutSource.Canvas) - layoutMutations.deleteReroute(id) + this.layoutMutations.setSource(LayoutSource.Canvas) + this.layoutMutations.deleteReroute(id) // This does not belong here; it should be handled by the caller, or run by a remove-many API. // https://github.com/Comfy-Org/litegraph.js/issues/898 @@ -2263,8 +2270,8 @@ export class LGraph if (!reroute.validateLinks(this._links, this.floatingLinks)) { this.reroutes.delete(reroute.id) // Clean up layout store - layoutMutations.setSource(LayoutSource.Canvas) - layoutMutations.deleteReroute(reroute.id) + this.layoutMutations.setSource(LayoutSource.Canvas) + this.layoutMutations.deleteReroute(reroute.id) } } diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 208c944f27..f4302af72a 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -5,7 +5,6 @@ import { calculateInputSlotPosFromSlot, calculateOutputSlotPos } from '@/renderer/core/canvas/litegraph/SlotCalculations' -import { layoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' import type { DragAndScale } from './DragAndScale' @@ -2845,8 +2844,8 @@ export class LGraphNode graph._links.set(link.id, link) // Register link in Layout Store for spatial tracking - layoutMutations.setSource(LayoutSource.Canvas) - layoutMutations.createLink( + graph.layoutMutations.setSource(LayoutSource.Canvas) + graph.layoutMutations.createLink( link.id, this.id, outputIndex, diff --git a/src/lib/litegraph/src/LLink.ts b/src/lib/litegraph/src/LLink.ts index e8bb419aeb..868524df3e 100644 --- a/src/lib/litegraph/src/LLink.ts +++ b/src/lib/litegraph/src/LLink.ts @@ -2,7 +2,7 @@ import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants' -import { layoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' +import { useLayoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' import type { LGraphNode, NodeId } from './LGraphNode' @@ -22,6 +22,8 @@ import type { SubgraphIO } from './types/serialisation' +const layoutMutations = useLayoutMutations() + export type LinkId = number export type SerialisedLLinkArray = [ diff --git a/src/lib/litegraph/src/Reroute.ts b/src/lib/litegraph/src/Reroute.ts index 7ea251901f..06180b657d 100644 --- a/src/lib/litegraph/src/Reroute.ts +++ b/src/lib/litegraph/src/Reroute.ts @@ -1,4 +1,4 @@ -import { layoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' +import { useLayoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' import { LGraphBadge } from './LGraphBadge' @@ -18,6 +18,8 @@ import type { import { distance, isPointInRect } from './measure' import type { Serialisable, SerialisableReroute } from './types/serialisation' +const layoutMutations = useLayoutMutations() + export type RerouteId = number /** The input or output slot that an incomplete reroute link is connected to. */ diff --git a/src/renderer/core/layout/operations/LayoutMutations.ts b/src/renderer/core/layout/operations/LayoutMutations.ts index b482f7bf75..2b8668e6a2 100644 --- a/src/renderer/core/layout/operations/LayoutMutations.ts +++ b/src/renderer/core/layout/operations/LayoutMutations.ts @@ -8,7 +8,6 @@ import log from 'loglevel' import { layoutStore } from '@/renderer/core/layout/store/LayoutStore' import { - type LayoutMutations, LayoutSource, type NodeId, type NodeLayout, @@ -18,25 +17,70 @@ import { const logger = log.getLogger('LayoutMutations') -class LayoutMutationsImpl implements LayoutMutations { +export interface LayoutMutations { + // Single node operations (synchronous, CRDT-ready) + moveNode(nodeId: NodeId, position: Point): void + resizeNode(nodeId: NodeId, size: Size): void + setNodeZIndex(nodeId: NodeId, zIndex: number): void + + // Node lifecycle operations + createNode(nodeId: NodeId, layout: Partial): void + deleteNode(nodeId: NodeId): void + + // Link operations + createLink( + linkId: string | number, + sourceNodeId: string | number, + sourceSlot: number, + targetNodeId: string | number, + targetSlot: number + ): void + deleteLink(linkId: string | number): void + + // Reroute operations + createReroute( + rerouteId: string | number, + position: Point, + parentId?: string | number, + linkIds?: (string | number)[] + ): void + deleteReroute(rerouteId: string | number): void + moveReroute( + rerouteId: string | number, + position: Point, + previousPosition: Point + ): void + + // Stacking operations + bringNodeToFront(nodeId: NodeId): void + + // Source tracking + setSource(source: LayoutSource): void + setActor(actor: string): void +} + +/** + * Composable for accessing layout mutations with clean destructuring API + */ +export function useLayoutMutations(): LayoutMutations { /** * Set the current mutation source */ - setSource(source: LayoutSource): void { + const setSource = (source: LayoutSource): void => { layoutStore.setSource(source) } /** * Set the current actor (for CRDT) */ - setActor(actor: string): void { + const setActor = (actor: string): void => { layoutStore.setActor(actor) } /** * Move a node to a new position */ - moveNode(nodeId: NodeId, position: Point): void { + const moveNode = (nodeId: NodeId, position: Point): void => { const existing = layoutStore.getNodeLayoutRef(nodeId).value if (!existing) return @@ -55,7 +99,7 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Resize a node */ - resizeNode(nodeId: NodeId, size: Size): void { + const resizeNode = (nodeId: NodeId, size: Size): void => { const existing = layoutStore.getNodeLayoutRef(nodeId).value if (!existing) return @@ -74,7 +118,7 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Set node z-index */ - setNodeZIndex(nodeId: NodeId, zIndex: number): void { + const setNodeZIndex = (nodeId: NodeId, zIndex: number): void => { const existing = layoutStore.getNodeLayoutRef(nodeId).value if (!existing) return @@ -93,7 +137,7 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Create a new node */ - createNode(nodeId: NodeId, layout: Partial): void { + const createNode = (nodeId: NodeId, layout: Partial): void => { const fullLayout: NodeLayout = { id: nodeId, position: layout.position ?? { x: 0, y: 0 }, @@ -122,7 +166,7 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Delete a node */ - deleteNode(nodeId: NodeId): void { + const deleteNode = (nodeId: NodeId): void => { const existing = layoutStore.getNodeLayoutRef(nodeId).value if (!existing) return @@ -140,7 +184,7 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Bring a node to the front (highest z-index) */ - bringNodeToFront(nodeId: NodeId): void { + const bringNodeToFront = (nodeId: NodeId): void => { // Get all nodes to find the highest z-index const allNodes = layoutStore.getAllNodes().value let maxZIndex = 0 @@ -152,19 +196,19 @@ class LayoutMutationsImpl implements LayoutMutations { } // Set this node's z-index to be one higher than the current max - this.setNodeZIndex(nodeId, maxZIndex + 1) + setNodeZIndex(nodeId, maxZIndex + 1) } /** * Create a new link */ - createLink( + const createLink = ( linkId: string | number, sourceNodeId: string | number, sourceSlot: number, targetNodeId: string | number, targetSlot: number - ): void { + ): void => { // Normalize node IDs to strings const normalizedSourceNodeId = String(sourceNodeId) const normalizedTargetNodeId = String(targetNodeId) @@ -191,7 +235,7 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Delete a link */ - deleteLink(linkId: string | number): void { + const deleteLink = (linkId: string | number): void => { logger.debug('Deleting link:', Number(linkId)) layoutStore.applyOperation({ type: 'deleteLink', @@ -206,12 +250,12 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Create a new reroute */ - createReroute( + const createReroute = ( rerouteId: string | number, position: Point, parentId?: string | number, linkIds: (string | number)[] = [] - ): void { + ): void => { logger.debug('Creating reroute:', { rerouteId: Number(rerouteId), position, @@ -234,7 +278,7 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Delete a reroute */ - deleteReroute(rerouteId: string | number): void { + const deleteReroute = (rerouteId: string | number): void => { logger.debug('Deleting reroute:', Number(rerouteId)) layoutStore.applyOperation({ type: 'deleteReroute', @@ -249,11 +293,11 @@ class LayoutMutationsImpl implements LayoutMutations { /** * Move a reroute */ - moveReroute( + const moveReroute = ( rerouteId: string | number, position: Point, previousPosition: Point - ): void { + ): void => { logger.debug('Moving reroute:', { rerouteId: Number(rerouteId), from: previousPosition, @@ -270,7 +314,20 @@ class LayoutMutationsImpl implements LayoutMutations { actor: layoutStore.getCurrentActor() }) } -} -// Create singleton instance -export const layoutMutations = new LayoutMutationsImpl() + return { + setSource, + setActor, + moveNode, + resizeNode, + setNodeZIndex, + createNode, + deleteNode, + bringNodeToFront, + createLink, + deleteLink, + createReroute, + deleteReroute, + moveReroute + } +} diff --git a/src/renderer/core/layout/sync/useLayout.ts b/src/renderer/core/layout/sync/useLayout.ts deleted file mode 100644 index 0545c9830b..0000000000 --- a/src/renderer/core/layout/sync/useLayout.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Main composable for accessing the layout system - * - * Provides unified access to the layout store and mutation API. - */ -import { layoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' -import { layoutStore } from '@/renderer/core/layout/store/LayoutStore' -import type { Bounds, NodeId, Point } from '@/renderer/core/layout/types' - -/** - * Main composable for accessing the layout system - */ -export function useLayout() { - return { - // Store access - store: layoutStore, - - // Mutation API - mutations: layoutMutations, - - // Reactive accessors - getNodeLayoutRef: (nodeId: NodeId) => layoutStore.getNodeLayoutRef(nodeId), - getAllNodes: () => layoutStore.getAllNodes(), - getNodesInBounds: (bounds: Bounds) => layoutStore.getNodesInBounds(bounds), - - // Non-reactive queries (for performance) - queryNodeAtPoint: (point: Point) => layoutStore.queryNodeAtPoint(point), - queryNodesInBounds: (bounds: Bounds) => - layoutStore.queryNodesInBounds(bounds) - } -} diff --git a/src/renderer/core/layout/types.ts b/src/renderer/core/layout/types.ts index 8f963ec569..cdd3e55a20 100644 --- a/src/renderer/core/layout/types.ts +++ b/src/renderer/core/layout/types.ts @@ -468,49 +468,6 @@ export interface LayoutStore { getCurrentActor(): string } -// Simplified mutation API -export interface LayoutMutations { - // Single node operations (synchronous, CRDT-ready) - moveNode(nodeId: NodeId, position: Point): void - resizeNode(nodeId: NodeId, size: Size): void - setNodeZIndex(nodeId: NodeId, zIndex: number): void - - // Node lifecycle operations - createNode(nodeId: NodeId, layout: Partial): void - deleteNode(nodeId: NodeId): void - - // Link operations - createLink( - linkId: string | number, - sourceNodeId: string | number, - sourceSlot: number, - targetNodeId: string | number, - targetSlot: number - ): void - deleteLink(linkId: string | number): void - - // Reroute operations - createReroute( - rerouteId: string | number, - position: Point, - parentId?: string | number, - linkIds?: (string | number)[] - ): void - deleteReroute(rerouteId: string | number): void - moveReroute( - rerouteId: string | number, - position: Point, - previousPosition: Point - ): void - - // Stacking operations - bringNodeToFront(nodeId: NodeId): void - - // Source tracking - setSource(source: LayoutSource): void - setActor(actor: string): void // For CRDT -} - // CRDT-ready operation log (for future CRDT integration) export interface OperationLog { operations: LayoutOperation[] diff --git a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts index efd0797868..5874a8048b 100644 --- a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts +++ b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts @@ -6,7 +6,7 @@ */ import { computed, inject } from 'vue' -import { layoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' +import { useLayoutMutations } from '@/renderer/core/layout/operations/LayoutMutations' import { layoutStore } from '@/renderer/core/layout/store/LayoutStore' import { LayoutSource, type Point } from '@/renderer/core/layout/types' @@ -16,7 +16,7 @@ import { LayoutSource, type Point } from '@/renderer/core/layout/types' */ export function useNodeLayout(nodeId: string) { const store = layoutStore - const mutations = layoutMutations + const mutations = useLayoutMutations() // Get transform utilities from TransformPane if available const transformState = inject('transformState') as diff --git a/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap b/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap index d3e6de7b2e..f7dda30c82 100644 --- a/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap +++ b/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap @@ -281,6 +281,21 @@ LGraph { "id": "b4e984f1-b421-4d24-b8b4-ff895793af13", "iteration": 0, "last_update_time": 0, + "layoutMutations": { + "bringNodeToFront": [Function], + "createLink": [Function], + "createNode": [Function], + "createReroute": [Function], + "deleteLink": [Function], + "deleteNode": [Function], + "deleteReroute": [Function], + "moveNode": [Function], + "moveReroute": [Function], + "resizeNode": [Function], + "setActor": [Function], + "setNodeZIndex": [Function], + "setSource": [Function], + }, "links": Map {}, "list_of_graphcanvas": null, "nodes_actioning": [],