Skip to content

Commit a8483a8

Browse files
feat: snap group borders to the grid when fitting to nodes (#15070)
## Summary Fitting a group to its nodes now leaves all four group borders on the grid when snap-to-grid is enabled, instead of landing at arbitrary sub-grid offsets. ## Changes - **What**: `LGraphGroup.resizeTo` expands the fitted bounds out to the nearest grid lines when `LiteGraph.alwaysSnapToGrid` is set. The expansion lives in a new pure helper, `expandRectToGrid` in `measure.ts`, alongside `snapPoint`. ## Review Focus The rect is only ever grown, never shrunk — left/top floor, right/bottom ceil — so the requested padding is never eaten and nothing that was inside the group before the call ends up outside it. Rounding to nearest would have been closer to `snapPoint`'s existing behaviour but can pull a border inside the padding it just added. Gated on `alwaysSnapToGrid` rather than on `getSnapToGridSize()` alone: that getter returns `CANVAS_GRID_SIZE` regardless of the setting, so using it by itself would snap groups for users who have snap-to-grid switched off. Applies to every fit path, since all of them funnel through `resizeTo` — context menu, more-options menu, `useCoreCommands`, and `useFrameNodes`. Fixes #1185
1 parent 8f221cd commit a8483a8

4 files changed

Lines changed: 99 additions & 2 deletions

File tree

src/lib/litegraph/src/LGraphGroup.test.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { describe, expect, vi } from 'vitest'
1+
import { afterEach, describe, expect, vi } from 'vitest'
22

33
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
4-
import { LGraph, LGraphGroup } from '@/lib/litegraph/src/litegraph'
4+
import { LGraph, LGraphGroup, LiteGraph } from '@/lib/litegraph/src/litegraph'
5+
import { containsRect } from '@/lib/litegraph/src/measure'
56
import * as colorUtil from '@/utils/colorUtil'
67

78
import { test } from './__fixtures__/testExtensions'
@@ -105,6 +106,49 @@ describe('LGraphGroup', () => {
105106
})
106107
})
107108

109+
describe('resizeTo', () => {
110+
const alwaysSnapToGrid = LiteGraph.alwaysSnapToGrid
111+
const gridSize = LiteGraph.CANVAS_GRID_SIZE
112+
113+
afterEach(() => {
114+
LiteGraph.alwaysSnapToGrid = alwaysSnapToGrid
115+
LiteGraph.CANVAS_GRID_SIZE = gridSize
116+
})
117+
118+
function createGroupFittedToContent() {
119+
const graph = new LGraph()
120+
const group = new LGraphGroup('group')
121+
graph.add(group)
122+
123+
const content = new LGraphGroup('content')
124+
content.pos = [103, 207]
125+
content.size = [140, 80]
126+
127+
group.resizeTo([content], 10)
128+
return { group, content }
129+
}
130+
131+
test('fits the group around its contents with padding', () => {
132+
LiteGraph.alwaysSnapToGrid = false
133+
const { group } = createGroupFittedToContent()
134+
135+
expect([...group.pos]).toEqual([93, 197 - group.titleHeight])
136+
expect([...group.size]).toEqual([160, 100 + group.titleHeight])
137+
})
138+
139+
test('expands every border to the grid when always snapping', () => {
140+
LiteGraph.alwaysSnapToGrid = true
141+
LiteGraph.CANVAS_GRID_SIZE = 10
142+
const { group, content } = createGroupFittedToContent()
143+
144+
const [x, y, width, height] = group.boundingRect
145+
expect([x, y, x + width, y + height].map((edge) => edge % 10)).toEqual([
146+
0, 0, 0, 0
147+
])
148+
expect(containsRect(group.boundingRect, content.boundingRect)).toBe(true)
149+
})
150+
})
151+
108152
describe('draw', () => {
109153
test('lightens the title text for a very dark background', () => {
110154
const group = new LGraphGroup('Group')

src/lib/litegraph/src/LGraphGroup.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
containsCentre,
2020
containsRect,
2121
createBounds,
22+
expandRectToGrid,
2223
isInRect,
2324
isInRectangle,
2425
isPointInRect,
@@ -325,6 +326,9 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
325326

326327
/**
327328
* Resizes and moves the group to neatly fit all given {@link objects}.
329+
*
330+
* When {@link LiteGraph.alwaysSnapToGrid} is enabled, the group is then
331+
* expanded so that all four of its borders line up with the grid.
328332
* @param objects All objects that should be inside the group
329333
* @param padding Value in graph units to add to all sides of the group. Default: 10
330334
*/
@@ -336,6 +340,11 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
336340
this.pos[1] = boundingBox[1] - this.titleHeight
337341
this.size[0] = boundingBox[2]
338342
this.size[1] = boundingBox[3] + this.titleHeight
343+
344+
const snapTo = LiteGraph.alwaysSnapToGrid
345+
? this.graph?.getSnapToGridSize()
346+
: undefined
347+
if (snapTo) expandRectToGrid(this._bounding, snapTo)
339348
}
340349

341350
/**

src/lib/litegraph/src/measure.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createBounds,
1010
dist2,
1111
distance,
12+
expandRectToGrid,
1213
findPointOnCurve,
1314
getOrientation,
1415
isInRect,
@@ -144,6 +145,24 @@ test('snapPoint correctly snaps points to grid using ceil', ({ expect }) => {
144145
expect(point3).toEqual([20, -10])
145146
})
146147

148+
test('expandRectToGrid grows every edge out to the grid', ({ expect }) => {
149+
const rect: Rect = [12.3, 18.7, 20, 20]
150+
expect(expandRectToGrid(rect, 10)).toBe(true)
151+
expect(rect).toEqual([10, 10, 30, 30])
152+
153+
const alreadyAligned: Rect = [10, 20, 30, 40]
154+
expect(expandRectToGrid(alreadyAligned, 10)).toBe(true)
155+
expect(alreadyAligned).toEqual([10, 20, 30, 40])
156+
157+
const negative: Rect = [-12.3, -18.7, 5, 5]
158+
expect(expandRectToGrid(negative, 10)).toBe(true)
159+
expect(negative).toEqual([-20, -20, 20, 10])
160+
161+
const unsnapped: Rect = [12.3, 18.7, 20, 20]
162+
expect(expandRectToGrid(unsnapped, 0)).toBe(false)
163+
expect(unsnapped).toEqual([12.3, 18.7, 20, 20])
164+
})
165+
147166
test('snapPoint correctly snaps points to grid using floor', ({ expect }) => {
148167
const point: Point = [12.3, 18.7]
149168
expect(snapPoint(point, 5, 'floor')).toBe(true)

src/lib/litegraph/src/measure.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,31 @@ export function snapPoint(
363363
return true
364364
}
365365

366+
/**
367+
* Expands a {@link Rect} outwards so that all four edges lie on a grid of size
368+
* {@link snapTo}.
369+
*
370+
* Unlike {@link snapPoint}, the rect is only ever grown, never shrunk, so
371+
* anything it contained before the call is still contained afterwards.
372+
* @param rect The rect that will be expanded, modified in place
373+
* @param snapTo The grid size to expand out to (multiples thereof)
374+
* @returns `true` if snapTo is truthy, otherwise `false`
375+
*/
376+
export function expandRectToGrid(rect: Rect, snapTo: number): boolean {
377+
if (!snapTo) return false
378+
379+
const right = snapTo * Math.ceil((rect[0] + rect[2]) / snapTo)
380+
const bottom = snapTo * Math.ceil((rect[1] + rect[3]) / snapTo)
381+
const left = snapTo * Math.floor(rect[0] / snapTo)
382+
const top = snapTo * Math.floor(rect[1] / snapTo)
383+
384+
rect[0] = left
385+
rect[1] = top
386+
rect[2] = right - left
387+
rect[3] = bottom - top
388+
return true
389+
}
390+
366391
/**
367392
* Aligns a {@link Rect} relative to the edges or centre of a {@link container} rectangle.
368393
*

0 commit comments

Comments
 (0)