Skip to content
Merged
16 changes: 14 additions & 2 deletions src/lib/litegraph/src/LGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -2593,7 +2594,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')
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2959,6 +2959,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>

// 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')) {
Expand Down
28 changes: 28 additions & 0 deletions src/lib/litegraph/src/measure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
10 changes: 7 additions & 3 deletions src/lib/litegraph/src/measure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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],
Expand All @@ -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')
}
}
}

/**
Expand Down Expand Up @@ -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(
{
Expand All @@ -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()) {
Expand All @@ -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) {
Expand All @@ -96,7 +122,7 @@ export function ensureCorrectLayoutScale(
graph.outputNode as SubgraphOutputNode
]) {
if (ioNode) {
unprojectPosSize(ioNode, anchor)
unprojectPosSize(ioNode, anchor, graph)
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/scripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
Loading