mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): support grid size of 8 on canvas
- Support grid size of 8 on canvas - Internal canvas math works on 8 - Update gridlines rendering to show 64 spaced lines and 32/16/8 when zoomed in - Bbox manipulation defaults to grid of 64 - hold shift to get grid of 8 Besides being something we support internally, supporting 8 on canvas avoids a lot of hacky logic needed to work well with aspect ratios.
This commit is contained in:
parent
4f43eda09b
commit
cecee33bc0
@ -1,21 +1,36 @@
|
||||
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
|
||||
import { useToken } from '@chakra-ui/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { range } from 'lodash-es';
|
||||
import type { ReactElement } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Group, Line as KonvaLine } from 'react-konva';
|
||||
import { getArbitraryBaseColor } from 'theme/colors';
|
||||
|
||||
const selector = createMemoizedSelector([stateSelector], ({ canvas }) => {
|
||||
const { stageScale, stageCoordinates, stageDimensions } = canvas;
|
||||
return { stageScale, stageCoordinates, stageDimensions };
|
||||
});
|
||||
|
||||
const baseGridLineColor = getArbitraryBaseColor(27);
|
||||
const fineGridLineColor = getArbitraryBaseColor(18);
|
||||
|
||||
const IAICanvasGrid = () => {
|
||||
const { stageScale, stageCoordinates, stageDimensions } =
|
||||
useAppSelector(selector);
|
||||
const [gridLineColor] = useToken('colors', ['base.800']);
|
||||
|
||||
const gridSpacing = useMemo(() => {
|
||||
if (stageScale >= 2) {
|
||||
return 8;
|
||||
}
|
||||
if (stageScale >= 1 && stageScale < 2) {
|
||||
return 16;
|
||||
}
|
||||
if (stageScale >= 0.5 && stageScale < 1) {
|
||||
return 32;
|
||||
}
|
||||
return 64;
|
||||
}, [stageScale]);
|
||||
|
||||
const unscale = useCallback(
|
||||
(value: number) => {
|
||||
@ -40,15 +55,15 @@ const IAICanvasGrid = () => {
|
||||
};
|
||||
|
||||
const gridOffset = {
|
||||
x: Math.ceil(unscale(x) / 64) * 64,
|
||||
y: Math.ceil(unscale(y) / 64) * 64,
|
||||
x: Math.ceil(unscale(x) / gridSpacing) * gridSpacing,
|
||||
y: Math.ceil(unscale(y) / gridSpacing) * gridSpacing,
|
||||
};
|
||||
|
||||
const gridRect = {
|
||||
x1: -gridOffset.x,
|
||||
y1: -gridOffset.y,
|
||||
x2: unscale(width) - gridOffset.x + 64,
|
||||
y2: unscale(height) - gridOffset.y + 64,
|
||||
x2: unscale(width) - gridOffset.x + gridSpacing,
|
||||
y2: unscale(height) - gridOffset.y + gridSpacing,
|
||||
};
|
||||
|
||||
const gridFullRect = {
|
||||
@ -58,40 +73,50 @@ const IAICanvasGrid = () => {
|
||||
y2: Math.max(stageRect.y2, gridRect.y2),
|
||||
};
|
||||
|
||||
const fullRect = gridFullRect;
|
||||
|
||||
const // find the x & y size of the grid
|
||||
xSize = fullRect.x2 - fullRect.x1,
|
||||
ySize = fullRect.y2 - fullRect.y1,
|
||||
xSize = gridFullRect.x2 - gridFullRect.x1,
|
||||
ySize = gridFullRect.y2 - gridFullRect.y1,
|
||||
// compute the number of steps required on each axis.
|
||||
xSteps = Math.round(xSize / 64) + 1,
|
||||
ySteps = Math.round(ySize / 64) + 1;
|
||||
xSteps = Math.round(xSize / gridSpacing) + 1,
|
||||
ySteps = Math.round(ySize / gridSpacing) + 1;
|
||||
|
||||
const xLines = range(0, xSteps).map((i) => (
|
||||
<KonvaLine
|
||||
key={`x_${i}`}
|
||||
x={fullRect.x1 + i * 64}
|
||||
y={fullRect.y1}
|
||||
points={[0, 0, 0, ySize]}
|
||||
stroke={gridLineColor}
|
||||
strokeWidth={1}
|
||||
listening={false}
|
||||
/>
|
||||
));
|
||||
const yLines = range(0, ySteps).map((i) => (
|
||||
<KonvaLine
|
||||
key={`y_${i}`}
|
||||
x={fullRect.x1}
|
||||
y={fullRect.y1 + i * 64}
|
||||
points={[0, 0, xSize, 0]}
|
||||
stroke={gridLineColor}
|
||||
strokeWidth={1}
|
||||
listening={false}
|
||||
/>
|
||||
));
|
||||
const strokeWidth = unscale(1);
|
||||
|
||||
return xLines.concat(yLines);
|
||||
}, [stageCoordinates, stageDimensions, unscale, gridLineColor]);
|
||||
const gridLines: ReactElement[] = new Array(xSteps + ySteps);
|
||||
let _x = 0;
|
||||
let _y = 0;
|
||||
for (let i = 0; i < xSteps; i++) {
|
||||
_x = gridFullRect.x1 + i * gridSpacing;
|
||||
gridLines.push(
|
||||
<KonvaLine
|
||||
key={`x_${i}`}
|
||||
x={_x}
|
||||
y={gridFullRect.y1}
|
||||
points={[0, 0, 0, ySize]}
|
||||
stroke={_x % 64 ? fineGridLineColor : baseGridLineColor}
|
||||
strokeWidth={strokeWidth}
|
||||
listening={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < ySteps; i++) {
|
||||
_y = gridFullRect.y1 + i * gridSpacing;
|
||||
gridLines.push(
|
||||
<KonvaLine
|
||||
key={`y_${i}`}
|
||||
x={gridFullRect.x1}
|
||||
y={_y}
|
||||
points={[0, 0, xSize, 0]}
|
||||
stroke={_y % 64 ? fineGridLineColor : baseGridLineColor}
|
||||
strokeWidth={strokeWidth}
|
||||
listening={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return gridLines;
|
||||
}, [stageDimensions, stageCoordinates, unscale, gridSpacing]);
|
||||
|
||||
return <Group listening={false}>{gridLines}</Group>;
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { $shift } from 'common/hooks/useGlobalModifiers';
|
||||
import {
|
||||
roundDownToMultiple,
|
||||
roundToMultiple,
|
||||
@ -15,6 +16,8 @@ import {
|
||||
setIsTransformingBoundingBox,
|
||||
} from 'features/canvas/store/canvasNanostore';
|
||||
import {
|
||||
CANVAS_GRID_SIZE_COARSE,
|
||||
CANVAS_GRID_SIZE_FINE,
|
||||
setBoundingBoxCoordinates,
|
||||
setBoundingBoxDimensions,
|
||||
setShouldSnapToGrid,
|
||||
@ -23,7 +26,7 @@ import type Konva from 'konva';
|
||||
import type { GroupConfig } from 'konva/lib/Group';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Group, Rect, Transformer } from 'react-konva';
|
||||
|
||||
@ -58,12 +61,10 @@ type IAICanvasBoundingBoxPreviewProps = GroupConfig;
|
||||
|
||||
const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
const { ...rest } = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
boundingBoxCoordinates,
|
||||
boundingBoxDimensions,
|
||||
|
||||
stageScale,
|
||||
shouldSnapToGrid,
|
||||
tool,
|
||||
@ -73,6 +74,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
|
||||
const transformerRef = useRef<Konva.Transformer>(null);
|
||||
const shapeRef = useRef<Konva.Rect>(null);
|
||||
const shift = useStore($shift);
|
||||
const isDrawing = useStore($isDrawing);
|
||||
const isMovingBoundingBox = useStore($isMovingBoundingBox);
|
||||
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
|
||||
@ -87,7 +89,14 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}, []);
|
||||
|
||||
const scaledStep = 64 * stageScale;
|
||||
const gridSize = useMemo(
|
||||
() => (shift ? CANVAS_GRID_SIZE_FINE : CANVAS_GRID_SIZE_COARSE),
|
||||
[shift]
|
||||
);
|
||||
const scaledStep = useMemo(
|
||||
() => gridSize * stageScale,
|
||||
[gridSize, stageScale]
|
||||
);
|
||||
|
||||
useHotkeys('N', () => {
|
||||
dispatch(setShouldSnapToGrid(!shouldSnapToGrid));
|
||||
@ -108,8 +117,8 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
const dragX = e.target.x();
|
||||
const dragY = e.target.y();
|
||||
|
||||
const newX = roundToMultiple(dragX, 64);
|
||||
const newY = roundToMultiple(dragY, 64);
|
||||
const newX = roundToMultiple(dragX, gridSize);
|
||||
const newY = roundToMultiple(dragY, gridSize);
|
||||
|
||||
e.target.x(newX);
|
||||
e.target.y(newY);
|
||||
@ -121,7 +130,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, shouldSnapToGrid]
|
||||
[dispatch, gridSize, shouldSnapToGrid]
|
||||
);
|
||||
|
||||
const handleOnTransform = useCallback(() => {
|
||||
@ -147,7 +156,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
const y = Math.round(rect.y());
|
||||
|
||||
if (aspectRatio) {
|
||||
const newHeight = roundToMultiple(width / aspectRatio.value, 64);
|
||||
const newHeight = roundToMultiple(width / aspectRatio.value, gridSize);
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
width: width,
|
||||
@ -165,15 +174,15 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||
|
||||
dispatch(
|
||||
setBoundingBoxCoordinates({
|
||||
x: shouldSnapToGrid ? roundDownToMultiple(x, 64) : x,
|
||||
y: shouldSnapToGrid ? roundDownToMultiple(y, 64) : y,
|
||||
x: shouldSnapToGrid ? roundDownToMultiple(x, gridSize) : x,
|
||||
y: shouldSnapToGrid ? roundDownToMultiple(y, gridSize) : y,
|
||||
})
|
||||
);
|
||||
|
||||
// Reset the scale now that the coords/dimensions have been un-scaled
|
||||
rect.scaleX(1);
|
||||
rect.scaleY(1);
|
||||
}, [dispatch, shouldSnapToGrid, aspectRatio]);
|
||||
}, [aspectRatio, dispatch, shouldSnapToGrid, gridSize]);
|
||||
|
||||
const anchorDragBoundFunc = useCallback(
|
||||
(
|
||||
|
@ -9,7 +9,7 @@ import calculateScale from 'features/canvas/util/calculateScale';
|
||||
import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants';
|
||||
import floorCoordinates from 'features/canvas/util/floorCoordinates';
|
||||
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
|
||||
import roundDimensionsTo64 from 'features/canvas/util/roundDimensionsTo64';
|
||||
import roundDimensionsToMultiple from 'features/canvas/util/roundDimensionsToMultiple';
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import { clamp, cloneDeep } from 'lodash-es';
|
||||
@ -40,6 +40,9 @@ import {
|
||||
*/
|
||||
const MAX_HISTORY = 128;
|
||||
|
||||
export const CANVAS_GRID_SIZE_FINE = 8;
|
||||
export const CANVAS_GRID_SIZE_COARSE = 64;
|
||||
|
||||
export const initialLayerState: CanvasLayerState = {
|
||||
objects: [],
|
||||
stagingArea: {
|
||||
@ -141,15 +144,24 @@ export const canvasSlice = createSlice({
|
||||
const { stageDimensions } = state;
|
||||
|
||||
const newBoundingBoxDimensions = {
|
||||
width: roundDownToMultiple(clamp(width, 64, 512), 64),
|
||||
height: roundDownToMultiple(clamp(height, 64, 512), 64),
|
||||
width: roundDownToMultiple(
|
||||
clamp(width, CANVAS_GRID_SIZE_FINE, 512),
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
height: roundDownToMultiple(
|
||||
clamp(height, CANVAS_GRID_SIZE_FINE, 512),
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
};
|
||||
|
||||
const newBoundingBoxCoordinates = {
|
||||
x: roundToMultiple(width / 2 - newBoundingBoxDimensions.width / 2, 64),
|
||||
x: roundToMultiple(
|
||||
width / 2 - newBoundingBoxDimensions.width / 2,
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
y: roundToMultiple(
|
||||
height / 2 - newBoundingBoxDimensions.height / 2,
|
||||
64
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
};
|
||||
|
||||
@ -206,10 +218,13 @@ export const canvasSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<Partial<Dimensions>>
|
||||
) => {
|
||||
const newDimensions = roundDimensionsTo64({
|
||||
...state.boundingBoxDimensions,
|
||||
...action.payload,
|
||||
});
|
||||
const newDimensions = roundDimensionsToMultiple(
|
||||
{
|
||||
...state.boundingBoxDimensions,
|
||||
...action.payload,
|
||||
},
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
);
|
||||
state.boundingBoxDimensions = newDimensions;
|
||||
|
||||
if (state.boundingBoxScaleMethod === 'auto') {
|
||||
@ -649,18 +664,24 @@ export const canvasSlice = createSlice({
|
||||
scaledStageHeight
|
||||
) {
|
||||
const newBoundingBoxDimensions = {
|
||||
width: roundDownToMultiple(clamp(scaledStageWidth, 64, 512), 64),
|
||||
height: roundDownToMultiple(clamp(scaledStageHeight, 64, 512), 64),
|
||||
width: roundDownToMultiple(
|
||||
clamp(scaledStageWidth, CANVAS_GRID_SIZE_FINE, 512),
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
height: roundDownToMultiple(
|
||||
clamp(scaledStageHeight, CANVAS_GRID_SIZE_FINE, 512),
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
};
|
||||
|
||||
const newBoundingBoxCoordinates = {
|
||||
x: roundToMultiple(
|
||||
scaledStageWidth / 2 - newBoundingBoxDimensions.width / 2,
|
||||
64
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
y: roundToMultiple(
|
||||
scaledStageHeight / 2 - newBoundingBoxDimensions.height / 2,
|
||||
64
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
),
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/canvasSlice';
|
||||
import type { Dimensions } from 'features/canvas/store/canvasTypes';
|
||||
|
||||
const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
|
||||
@ -10,7 +11,7 @@ const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
|
||||
let currentArea = width * height;
|
||||
let maxDimension = 448;
|
||||
while (currentArea < targetArea) {
|
||||
maxDimension += 64;
|
||||
maxDimension += CANVAS_GRID_SIZE_FINE;
|
||||
if (width === height) {
|
||||
scaledDimensions.width = 512;
|
||||
scaledDimensions.height = 512;
|
||||
@ -20,13 +21,13 @@ const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
|
||||
scaledDimensions.width = maxDimension;
|
||||
scaledDimensions.height = roundToMultiple(
|
||||
maxDimension / aspectRatio,
|
||||
64
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
);
|
||||
} else if (aspectRatio < 1) {
|
||||
scaledDimensions.height = maxDimension;
|
||||
scaledDimensions.width = roundToMultiple(
|
||||
maxDimension * aspectRatio,
|
||||
64
|
||||
CANVAS_GRID_SIZE_FINE
|
||||
);
|
||||
}
|
||||
currentArea = scaledDimensions.width * scaledDimensions.height;
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import type { Dimensions } from 'features/canvas/store/canvasTypes';
|
||||
|
||||
const roundDimensionsTo64 = (dimensions: Dimensions): Dimensions => {
|
||||
return {
|
||||
width: roundToMultiple(dimensions.width, 64),
|
||||
height: roundToMultiple(dimensions.height, 64),
|
||||
};
|
||||
};
|
||||
|
||||
export default roundDimensionsTo64;
|
@ -0,0 +1,11 @@
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import type { Dimensions } from 'features/canvas/store/canvasTypes';
|
||||
|
||||
const roundDimensionsToMultiple = (dimensions: Dimensions, multiple: number): Dimensions => {
|
||||
return {
|
||||
width: roundToMultiple(dimensions.width, multiple),
|
||||
height: roundToMultiple(dimensions.height, multiple),
|
||||
};
|
||||
};
|
||||
|
||||
export default roundDimensionsToMultiple;
|
@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/canvasSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -46,7 +47,8 @@ const ParamBoundingBoxWidth = () => {
|
||||
<InvSlider
|
||||
min={64}
|
||||
max={1536}
|
||||
step={64}
|
||||
step={CANVAS_GRID_SIZE_COARSE}
|
||||
fineStep={CANVAS_GRID_SIZE_FINE}
|
||||
value={ctx.height}
|
||||
onChange={onChange}
|
||||
onReset={onReset}
|
||||
|
@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/canvasSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -46,7 +47,8 @@ const ParamBoundingBoxWidth = () => {
|
||||
<InvSlider
|
||||
min={64}
|
||||
max={1536}
|
||||
step={64}
|
||||
step={CANVAS_GRID_SIZE_COARSE}
|
||||
fineStep={CANVAS_GRID_SIZE_FINE}
|
||||
value={ctx.width}
|
||||
onChange={onChange}
|
||||
onReset={onReset}
|
||||
|
@ -13,6 +13,9 @@ const BLUE = { H: 200, S: 76 };
|
||||
const GREEN = { H: 110, S: 69 };
|
||||
const RED = { H: 16, S: 92 };
|
||||
|
||||
export const getArbitraryBaseColor = (lightness: number) =>
|
||||
`hsl(${BASE.H} ${BASE.S}% ${lightness}%)`;
|
||||
|
||||
export const InvokeAIColors: InvokeAIThemeColors = {
|
||||
base: generateColorPalette(BASE.H, BASE.S),
|
||||
baseAlpha: generateColorPalette(BASE.H, BASE.S, true),
|
||||
|
Loading…
Reference in New Issue
Block a user