mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 09:27:41 +00:00
fix: respect 'always snap to grid' when auto-scale layout from nodes 1.0 to 2.0 (#9332)
## Summary Previously when I switch from nodes 1.0 to 2.0, positions and sizes of nodes do not follow 'always snap to grid'. You can guess what a mess it is for people relying on snap to grid to retain sanity. This PR fixes it. ## Changes In `ensureCorrectLayoutScale`, we call `snapPoint` after the position and the size are updated. We also need to ensure that the snapped size is larger than the minimal size required by the content, so I've added 'ceil' mode to `snapPoint`, and the patch is larger than I thought first. I'd happily try out nodes 2.0 once this is addressed :) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9332-fix-respect-always-snap-to-grid-when-auto-scale-layout-from-nodes-1-0-to-2-0-3176d73d365081f5b6bcc035a8ffa648) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user