diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index eafefbeba5..c768e54f12 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -45,7 +45,8 @@ import { LiteGraph, SubgraphNode } from './litegraph' import { alignOutsideContainer, alignToContainer, - createBounds + createBounds, + snapPoint } from './measure' import { SubgraphInput } from './subgraph/SubgraphInput' import { SubgraphInputNode } from './subgraph/SubgraphInputNode' @@ -2594,7 +2595,18 @@ export class LGraph // configure nodes afterwards so they can reach each other for (const [id, nodeData] of nodeDataMap) { - this.getNodeById(id)?.configure(nodeData) + const node = this.getNodeById(id) + node?.configure(nodeData) + + if (LiteGraph.alwaysSnapToGrid && node) { + const snapTo = this.getSnapToGridSize() + if (node.snapToGrid(snapTo)) { + // snapToGrid mutates the internal _pos array in-place, bypassing the setter + // This reassignment triggers the pos setter to sync to the Vue layout store + node.pos = [node.pos[0], node.pos[1]] + } + snapPoint(node.size, snapTo, 'ceil') + } } } diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index d52475a4ed..9815f36cd8 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -2959,6 +2959,11 @@ export class LGraphCanvas implements CustomEventDispatcher // Enforce minimum size const min = node.computeSize() + if (this._snapToGrid) { + // Previously newBounds.size is snapped with 'round' + // Now the minimum size is snapped with 'ceil' to avoid clipping + snapPoint(min, this._snapToGrid, 'ceil') + } if (newBounds.width < min[0]) { // If resizing from left, adjust position to maintain right edge if (resizeDirection.includes('W')) { diff --git a/src/lib/litegraph/src/measure.test.ts b/src/lib/litegraph/src/measure.test.ts index dc588a09f4..f642c0c7a9 100644 --- a/src/lib/litegraph/src/measure.test.ts +++ b/src/lib/litegraph/src/measure.test.ts @@ -130,6 +130,34 @@ test('snapPoint correctly snaps points to grid', ({ expect }) => { expect(point3).toEqual([20, 20]) }) +test('snapPoint correctly snaps points to grid using ceil', ({ expect }) => { + const point: Point = [12.3, 18.7] + expect(snapPoint(point, 5, 'ceil')).toBe(true) + expect(point).toEqual([15, 20]) + + const point2: Point = [15, 20] + expect(snapPoint(point2, 5, 'ceil')).toBe(true) + expect(point2).toEqual([15, 20]) + + const point3: Point = [15.1, -18.7] + expect(snapPoint(point3, 10, 'ceil')).toBe(true) + expect(point3).toEqual([20, -10]) +}) + +test('snapPoint correctly snaps points to grid using floor', ({ expect }) => { + const point: Point = [12.3, 18.7] + expect(snapPoint(point, 5, 'floor')).toBe(true) + expect(point).toEqual([10, 15]) + + const point2: Point = [15, 20] + expect(snapPoint(point2, 5, 'floor')).toBe(true) + expect(point2).toEqual([15, 20]) + + const point3: Point = [15.1, -18.7] + expect(snapPoint(point3, 10, 'floor')).toBe(true) + expect(point3).toEqual([10, -20]) +}) + test('createBounds correctly creates bounding box', ({ expect }) => { const objects = [ { boundingRect: [0, 0, 10, 10] as Rect }, diff --git a/src/lib/litegraph/src/measure.ts b/src/lib/litegraph/src/measure.ts index d6d81c83ab..08ce8a50bb 100644 --- a/src/lib/litegraph/src/measure.ts +++ b/src/lib/litegraph/src/measure.ts @@ -351,11 +351,15 @@ export function createBounds( * @returns `true` if snapTo is truthy, otherwise `false` * @remarks `NaN` propagates through this function and does not affect return value. */ -export function snapPoint(pos: Point | Rect, snapTo: number): boolean { +export function snapPoint( + pos: Point | Rect, + snapTo: number, + method: 'round' | 'ceil' | 'floor' = 'round' +): boolean { if (!snapTo) return false - pos[0] = snapTo * Math.round(pos[0] / snapTo) - pos[1] = snapTo * Math.round(pos[1] / snapTo) + pos[0] = snapTo * Math[method](pos[0] / snapTo) + pos[1] = snapTo * Math[method](pos[1] / snapTo) return true } diff --git a/src/renderer/extensions/vueNodes/composables/useNodeSnap.ts b/src/renderer/extensions/vueNodes/composables/useNodeSnap.ts index 4b07621bed..03476156ee 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodeSnap.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodeSnap.ts @@ -57,7 +57,7 @@ export function useNodeSnap() { if (!gridSizeValue) return { ...size } const sizeArray: [number, number] = [size.width, size.height] - if (snapPoint(sizeArray, gridSizeValue)) { + if (snapPoint(sizeArray, gridSizeValue, 'ceil')) { return { width: sizeArray[0], height: sizeArray[1] } } return { ...size } diff --git a/src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.ts b/src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.ts index ef4d353a67..2584b7e1bb 100644 --- a/src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.ts +++ b/src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.ts @@ -1,4 +1,6 @@ import type { LGraph, RendererType } from '@/lib/litegraph/src/LGraph' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { snapPoint } from '@/lib/litegraph/src/measure' import type { Point as LGPoint } from '@/lib/litegraph/src/interfaces' import type { Point } from '@/renderer/core/layout/types' import { @@ -15,7 +17,7 @@ interface Positioned { size: LGPoint } -function unprojectPosSize(item: Positioned, anchor: Point) { +function unprojectPosSize(item: Positioned, anchor: Point, graph: LGraph) { const c = unprojectBounds( { x: item.pos[0], @@ -30,6 +32,14 @@ function unprojectPosSize(item: Positioned, anchor: Point) { item.pos[1] = c.y item.size[0] = c.width item.size[1] = c.height + + if (LiteGraph.alwaysSnapToGrid) { + const snapTo = graph.getSnapToGridSize?.() + if (snapTo) { + snapPoint(item.pos, snapTo, 'round') + snapPoint(item.size, snapTo, 'ceil') + } + } } /** @@ -60,6 +70,18 @@ export function ensureCorrectLayoutScale( const anchor = getGraphRenderAnchor(graph) + const applySnap = ( + pos: [number, number], + method: 'round' | 'ceil' | 'floor' = 'round' + ) => { + if (LiteGraph.alwaysSnapToGrid) { + const snapTo = graph.getSnapToGridSize?.() + if (snapTo) { + snapPoint(pos, snapTo, method) + } + } + } + for (const node of graph.nodes) { const c = unprojectBounds( { @@ -75,6 +97,9 @@ export function ensureCorrectLayoutScale( node.pos[1] = c.y node.size[0] = c.width node.size[1] = c.height + + applySnap(node.pos) + applySnap(node.size, 'ceil') } for (const reroute of graph.reroutes.values()) { @@ -84,10 +109,11 @@ export function ensureCorrectLayoutScale( RENDER_SCALE_FACTOR ) reroute.pos = [p.x, p.y] + applySnap(reroute.pos) } for (const group of graph.groups) { - unprojectPosSize(group, anchor) + unprojectPosSize(group, anchor, graph) } if ('inputNode' in graph && 'outputNode' in graph) { @@ -96,7 +122,7 @@ export function ensureCorrectLayoutScale( graph.outputNode as SubgraphOutputNode ]) { if (ioNode) { - unprojectPosSize(ioNode, anchor) + unprojectPosSize(ioNode, anchor, graph) } } } diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 5eda2903fd..361a8b7a1d 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -17,6 +17,7 @@ import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' +import { snapPoint } from '@/lib/litegraph/src/measure' import type { Vector2 } from '@/lib/litegraph/src/litegraph' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { isCloud } from '@/platform/distribution/types' @@ -1309,10 +1310,14 @@ export class ComfyApp { console.error(error) return } + const snapTo = LiteGraph.alwaysSnapToGrid + ? this.rootGraph.getSnapToGridSize() + : 0 forEachNode(this.rootGraph, (node) => { const size = node.computeSize() size[0] = Math.max(node.size[0], size[0]) size[1] = Math.max(node.size[1], size[1]) + snapPoint(size, snapTo, 'ceil') node.setSize(size) if (node.widgets) { // If you break something in the backend and want to patch workflows in the frontend