Skip to content

Commit

Permalink
fix(ui): fit bbox to layers math
Browse files Browse the repository at this point in the history
  • Loading branch information
psychedelicious committed Nov 8, 2024
1 parent 75acece commit 2ef07c6
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import {
roundDownToMultiple,
roundToMultiple,
roundToMultipleMin,
roundUpToMultiple,
} from 'common/util/roundDownToMultiple';
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
import { getKonvaNodeDebugAttrs, getPrefixedId } from 'features/controlLayers/konva/util';
import { fitRectToGrid, getKonvaNodeDebugAttrs, getPrefixedId } from 'features/controlLayers/konva/util';
import { selectBboxOverlay } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectBbox } from 'features/controlLayers/store/selectors';
import type { Coordinate, Rect } from 'features/controlLayers/store/types';
Expand Down Expand Up @@ -398,18 +393,12 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
}

// Determine the bbox size that fits within the visible rect. The bbox must be at least 64px in width and height,
// and its width and height must be multiples of 8px.
// and its width and height must be multiples of the bbox grid size.
const gridSize = this.manager.stateApi.getBboxGridSize();

// To be conservative, we will round up the x and y to the nearest grid size, and round down the width and height.
// This ensures the bbox is never _larger_ than the visible rect. If the bbox is larger than the visible, we
// will always trigger the outpainting workflow, which is not what the user wants.
const x = roundUpToMultiple(visibleRect.x, gridSize);
const y = roundUpToMultiple(visibleRect.y, gridSize);
const width = roundDownToMultiple(visibleRect.width, gridSize);
const height = roundDownToMultiple(visibleRect.height, gridSize);
const rect = fitRectToGrid(visibleRect, gridSize);

this.manager.stateApi.setGenerationBbox({ x, y, width, height });
this.manager.stateApi.setGenerationBbox(rect);
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
import { roundUpToMultiple } from 'common/util/roundDownToMultiple';
import { fitRectToGrid, getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
import type { Rect } from 'features/controlLayers/store/types';
import { describe, expect, it } from 'vitest';

describe('util', () => {
Expand Down Expand Up @@ -44,4 +46,74 @@ describe('util', () => {
expect(union).toEqual({ x: 0, y: 0, width: 0, height: 0 });
});
});

describe('fitRectToGrid', () => {
it('should fit rect within grid without exceeding bounds', () => {
const rect: Rect = { x: 0, y: 0, width: 1047, height: 1758 };
const gridSize = 50;
const result = fitRectToGrid(rect, gridSize);

expect(result.x).toBe(roundUpToMultiple(rect.x, gridSize));
expect(result.y).toBe(roundUpToMultiple(rect.y, gridSize));
expect(result.width).toBeLessThanOrEqual(rect.width);
expect(result.height).toBeLessThanOrEqual(rect.height);
expect(result.width % gridSize).toBe(0);
expect(result.height % gridSize).toBe(0);
});

it('should handle small rect within grid bounds', () => {
const rect: Rect = { x: 20, y: 30, width: 80, height: 90 };
const gridSize = 25;
const result = fitRectToGrid(rect, gridSize);

expect(result.x).toBe(25);
expect(result.y).toBe(50);
expect(result.width % gridSize).toBe(0);
expect(result.height % gridSize).toBe(0);
expect(result.width).toBeLessThanOrEqual(rect.width);
expect(result.height).toBeLessThanOrEqual(rect.height);
});

it('should handle rect starting outside of grid alignment', () => {
const rect: Rect = { x: 13, y: 27, width: 94, height: 112 };
const gridSize = 20;
const result = fitRectToGrid(rect, gridSize);

expect(result.x).toBe(20);
expect(result.y).toBe(40);
expect(result.width % gridSize).toBe(0);
expect(result.height % gridSize).toBe(0);
expect(result.width).toBeLessThanOrEqual(rect.width);
expect(result.height).toBeLessThanOrEqual(rect.height);
});

it('should return the same rect if already aligned to grid', () => {
const rect: Rect = { x: 100, y: 100, width: 200, height: 300 };
const gridSize = 50;
const result = fitRectToGrid(rect, gridSize);

expect(result).toEqual(rect);
});

it('should handle large grid sizes relative to rect dimensions', () => {
const rect: Rect = { x: 250, y: 300, width: 400, height: 500 };
const gridSize = 100;
const result = fitRectToGrid(rect, gridSize);

expect(result.x).toBe(300);
expect(result.y).toBe(300);
expect(result.width % gridSize).toBe(0);
expect(result.height % gridSize).toBe(0);
expect(result.width).toBeLessThanOrEqual(rect.width);
expect(result.height).toBeLessThanOrEqual(rect.height);
});

it('should handle rect with zero width and height', () => {
const rect: Rect = { x: 40, y: 60, width: 100, height: 200 };
const gridSize = 20;
const result = fitRectToGrid(rect, gridSize);

expect(result).toEqual({ x: 40, y: 60, width: 100, height: 200 });
});
});
});
28 changes: 28 additions & 0 deletions invokeai/frontend/web/src/features/controlLayers/konva/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Selector, Store } from '@reduxjs/toolkit';
import { $authToken } from 'app/store/nanostores/authToken';
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
import type {
CanvasEntityIdentifier,
CanvasObjectState,
Expand Down Expand Up @@ -560,6 +561,33 @@ export const getRectIntersection = (...rects: Rect[]): Rect => {
return rect || getEmptyRect();
};

/**
* Fits a rect to the nearest multiple of the grid size, rounding down. The returned rect will be smaller than or equal
* to the input rect, and will be aligned to the grid.
*
* In other words, shrink the rect inwards on each size until it fits within the visible rect and aligns to the grid.
*
* @param rect The rect to fit
* @param gridSize The size of the grid
* @returns The fitted rect
*/
export const fitRectToGrid = (rect: Rect, gridSize: number): Rect => {
// Rounding x and y up effectively shrinks the left and top edges of the rect, and rounding width and height down
// effectively shrinks the right and bottom edges.
const x = roundUpToMultiple(rect.x, gridSize);
const y = roundUpToMultiple(rect.y, gridSize);

// Because we've just shifted the rect's x and y, we need to adjust the width and height by the same amount before
// we round those values down.
const offsetX = x - rect.x;
const offsetY = y - rect.y;

const width = roundDownToMultiple(rect.width - offsetX, gridSize);
const height = roundDownToMultiple(rect.height - offsetY, gridSize);

return { x, y, width, height };
};

/**
* Asserts that the value is never reached. Used for exhaustive checks in switch statements or conditional logic to ensure
* that all possible values are handled.
Expand Down

0 comments on commit 2ef07c6

Please sign in to comment.