feat(ui): add optimal size handling

This commit is contained in:
psychedelicious
2024-01-03 23:56:28 +11:00
committed by Kent Keirsey
parent 1a4be78013
commit 4fdc4c15f9
30 changed files with 461 additions and 416 deletions

View File

@ -26,6 +26,8 @@ import {
CANVAS_GRID_SIZE_COARSE,
CANVAS_GRID_SIZE_FINE,
} from 'features/canvas/store/constants';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import type Konva from 'konva';
import type { GroupConfig } from 'konva/lib/Group';
import type { KonvaEventObject } from 'konva/lib/Node';
@ -37,8 +39,8 @@ import { Group, Rect, Transformer } from 'react-konva';
const borderDash = [4, 4];
const boundingBoxPreviewSelector = createMemoizedSelector(
[stateSelector],
({ canvas }) => {
[stateSelector, selectOptimalDimension],
({ canvas }, optimalDimension) => {
const {
boundingBoxCoordinates,
boundingBoxDimensions,
@ -56,6 +58,7 @@ const boundingBoxPreviewSelector = createMemoizedSelector(
tool,
hitStrokeWidth: 20 / stageScale,
aspectRatio,
optimalDimension,
};
}
);
@ -73,6 +76,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
tool,
hitStrokeWidth,
aspectRatio,
optimalDimension,
} = useAppSelector(boundingBoxPreviewSelector);
const transformerRef = useRef<Konva.Transformer>(null);
@ -163,22 +167,25 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const y = Math.round(rect.y());
if (aspectRatio.isLocked) {
const newHeight = roundDownToMultipleMin(
width / aspectRatio.value,
gridSize
);
const newDimensions = calculateNewSize(aspectRatio.value, width * height);
dispatch(
setBoundingBoxDimensions({
width: roundDownToMultipleMin(width, gridSize),
height: newHeight,
})
setBoundingBoxDimensions(
{
width: roundDownToMultipleMin(newDimensions.width, gridSize),
height: roundDownToMultipleMin(newDimensions.height, gridSize),
},
optimalDimension
)
);
} else {
dispatch(
setBoundingBoxDimensions({
width: roundDownToMultipleMin(width, gridSize),
height: roundDownToMultipleMin(height, gridSize),
})
setBoundingBoxDimensions(
{
width: roundDownToMultipleMin(width, gridSize),
height: roundDownToMultipleMin(height, gridSize),
},
optimalDimension
)
);
dispatch(
aspectRatioChanged({
@ -205,6 +212,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
dispatch,
shouldSnapToGrid,
gridSize,
optimalDimension,
]);
const anchorDragBoundFunc = useCallback(
@ -233,7 +241,6 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
x: roundDownToMultiple(newPos.x, scaledStep) + offsetX,
y: roundDownToMultiple(newPos.y, scaledStep) + offsetY,
};
console.log({ oldPos, newPos, newCoordinates });
return newCoordinates;
},

View File

@ -11,6 +11,12 @@ import floorCoordinates from 'features/canvas/util/floorCoordinates';
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
import roundDimensionsToMultiple from 'features/canvas/util/roundDimensionsToMultiple';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { modelChanged } from 'features/parameters/store/generationSlice';
import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types';
import {
getIsSizeOptimal,
getOptimalDimension,
} from 'features/parameters/util/optimalDimension';
import type { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
import type { RgbaColor } from 'react-colorful';
@ -86,6 +92,29 @@ export const initialCanvasState: CanvasState = {
},
};
const setBoundingBoxDimensionsReducer = (
state: CanvasState,
payload: Partial<Dimensions>,
optimalDimension: number
) => {
const boundingBoxDimensions = payload;
const newDimensions = roundDimensionsToMultiple(
{
...state.boundingBoxDimensions,
...boundingBoxDimensions,
},
CANVAS_GRID_SIZE_FINE
);
state.boundingBoxDimensions = newDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newDimensions,
optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
};
export const canvasSlice = createSlice({
name: 'canvas',
initialState: initialCanvasState,
@ -132,117 +161,90 @@ export const canvasSlice = createSlice({
state.isMaskEnabled = action.payload;
state.layer = action.payload ? 'mask' : 'base';
},
setInitialCanvasImage: (state, action: PayloadAction<ImageDTO>) => {
const image = action.payload;
const { width, height } = image;
const { stageDimensions } = state;
setInitialCanvasImage: {
reducer: (state, action: PayloadActionWithOptimalDimension<ImageDTO>) => {
const { width, height, image_name } = action.payload;
const { optimalDimension } = action.meta;
const { stageDimensions } = state;
const newBoundingBoxDimensions = {
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 newBoundingBoxDimensions = {
width: roundDownToMultiple(
clamp(width, CANVAS_GRID_SIZE_FINE, optimalDimension),
CANVAS_GRID_SIZE_FINE
),
height: roundDownToMultiple(
clamp(height, CANVAS_GRID_SIZE_FINE, optimalDimension),
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxCoordinates = {
x: roundToMultiple(
width / 2 - newBoundingBoxDimensions.width / 2,
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
height / 2 - newBoundingBoxDimensions.height / 2,
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxCoordinates = {
x: roundToMultiple(
width / 2 - newBoundingBoxDimensions.width / 2,
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
height / 2 - newBoundingBoxDimensions.height / 2,
CANVAS_GRID_SIZE_FINE
),
};
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions,
optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
state.pastLayerStates.push(cloneDeep(state.layerState));
state.layerState = {
...cloneDeep(initialLayerState),
objects: [
{
kind: 'image',
layer: 'base',
x: 0,
y: 0,
width,
height,
imageName: image_name,
},
],
};
state.futureLayerStates = [];
state.batchIds = [];
const newScale = calculateScale(
stageDimensions.width,
stageDimensions.height,
width,
height,
STAGE_PADDING_PERCENTAGE
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
state.pastLayerStates.push(cloneDeep(state.layerState));
state.layerState = {
...cloneDeep(initialLayerState),
objects: [
{
kind: 'image',
layer: 'base',
x: 0,
y: 0,
width: width,
height: height,
imageName: image.image_name,
},
],
};
state.futureLayerStates = [];
state.batchIds = [];
const newScale = calculateScale(
stageDimensions.width,
stageDimensions.height,
width,
height,
STAGE_PADDING_PERCENTAGE
);
const newCoordinates = calculateCoordinates(
stageDimensions.width,
stageDimensions.height,
0,
0,
width,
height,
newScale
);
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
},
setBoundingBoxDimensions: (
state,
action: PayloadAction<Partial<Dimensions>>
) => {
const newDimensions = roundDimensionsToMultiple(
{
...state.boundingBoxDimensions,
...action.payload,
const newCoordinates = calculateCoordinates(
stageDimensions.width,
stageDimensions.height,
0,
0,
width,
height,
newScale
);
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
},
prepare: (payload: ImageDTO, optimalDimension: number) => ({
payload,
meta: {
optimalDimension,
},
CANVAS_GRID_SIZE_FINE
);
state.boundingBoxDimensions = newDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(newDimensions);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
},
flipBoundingBoxAxes: (state) => {
const [currWidth, currHeight] = [
state.boundingBoxDimensions.width,
state.boundingBoxDimensions.height,
];
const [currScaledWidth, currScaledHeight] = [
state.scaledBoundingBoxDimensions.width,
state.scaledBoundingBoxDimensions.height,
];
state.boundingBoxDimensions = {
width: currHeight,
height: currWidth,
};
state.scaledBoundingBoxDimensions = {
width: currScaledHeight,
height: currScaledWidth,
};
}),
},
setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => {
state.boundingBoxCoordinates = floorCoordinates(action.payload);
@ -518,38 +520,6 @@ export const canvasSlice = createSlice({
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
} else {
const newScale = calculateScale(
stageWidth,
stageHeight,
512,
512,
STAGE_PADDING_PERCENTAGE
);
const newCoordinates = calculateCoordinates(
stageWidth,
stageHeight,
0,
0,
512,
512,
newScale
);
const newBoundingBoxDimensions = { width: 512, height: 512 };
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
state.boundingBoxCoordinates = { x: 0, y: 0 };
state.boundingBoxDimensions = newBoundingBoxDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
}
},
nextStagingAreaImage: (state) => {
@ -601,69 +571,29 @@ export const canvasSlice = createSlice({
state.shouldShowStagingImage = true;
state.batchIds = [];
},
fitBoundingBoxToStage: (state) => {
const {
boundingBoxDimensions,
boundingBoxCoordinates,
stageDimensions,
stageScale,
} = state;
const scaledStageWidth = stageDimensions.width / stageScale;
const scaledStageHeight = stageDimensions.height / stageScale;
setBoundingBoxScaleMethod: {
reducer: (
state,
action: PayloadActionWithOptimalDimension<BoundingBoxScaleMethod>
) => {
const boundingBoxScaleMethod = action.payload;
const { optimalDimension } = action.meta;
state.boundingBoxScaleMethod = boundingBoxScaleMethod;
if (
boundingBoxCoordinates.x < 0 ||
boundingBoxCoordinates.x + boundingBoxDimensions.width >
scaledStageWidth ||
boundingBoxCoordinates.y < 0 ||
boundingBoxCoordinates.y + boundingBoxDimensions.height >
scaledStageHeight
) {
const newBoundingBoxDimensions = {
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,
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
scaledStageHeight / 2 - newBoundingBoxDimensions.height / 2,
CANVAS_GRID_SIZE_FINE
),
};
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
if (state.boundingBoxScaleMethod === 'auto') {
if (boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions
state.boundingBoxDimensions,
optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
}
},
setBoundingBoxScaleMethod: (
state,
action: PayloadAction<BoundingBoxScaleMethod>
) => {
state.boundingBoxScaleMethod = action.payload;
if (action.payload === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
state.boundingBoxDimensions
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
},
prepare: (payload: BoundingBoxScaleMethod, optimalDimension: number) => ({
payload,
meta: {
optimalDimension,
},
}),
},
setScaledBoundingBoxDimensions: (
state,
@ -671,6 +601,37 @@ export const canvasSlice = createSlice({
) => {
state.scaledBoundingBoxDimensions = action.payload;
},
setBoundingBoxDimensions: {
reducer: (
state,
action: PayloadActionWithOptimalDimension<Partial<Dimensions>>
) => {
setBoundingBoxDimensionsReducer(
state,
action.payload,
action.meta.optimalDimension
);
},
prepare: (payload: Partial<Dimensions>, optimalDimension: number) => ({
payload,
meta: {
optimalDimension,
},
}),
},
scaledBoundingBoxDimensionsReset: {
reducer: (state, action: PayloadActionWithOptimalDimension) => {
const scaledDimensions = getScaledBoundingBoxDimensions(
state.boundingBoxDimensions,
action.meta.optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
},
prepare: (payload: void, optimalDimension: number) => ({
payload: undefined,
meta: { optimalDimension },
}),
},
setShouldShowStagingImage: (state, action: PayloadAction<boolean>) => {
state.shouldShowStagingImage = action.payload;
},
@ -714,6 +675,22 @@ export const canvasSlice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(modelChanged, (state, action) => {
const optimalDimension = getOptimalDimension(action.payload);
const { width, height } = state.boundingBoxDimensions;
if (getIsSizeOptimal(width, height, optimalDimension)) {
return;
}
setBoundingBoxDimensionsReducer(
state,
{
width,
height,
},
optimalDimension
);
});
builder.addCase(appSocketQueueItemStatusChanged, (state, action) => {
const batch_status = action.payload.data.batch_status;
if (!state.batchIds.includes(batch_status.batch_id)) {
@ -754,7 +731,6 @@ export const {
commitColorPickerColor,
commitStagingAreaImage,
discardStagedImages,
fitBoundingBoxToStage,
nextStagingAreaImage,
prevStagingAreaImage,
redo,
@ -764,7 +740,6 @@ export const {
setBoundingBoxDimensions,
setBoundingBoxPreviewFill,
setBoundingBoxScaleMethod,
flipBoundingBoxAxes,
setBrushColor,
setBrushSize,
setColorPickerColor,
@ -799,6 +774,7 @@ export const {
canvasBatchIdAdded,
canvasBatchIdsReset,
aspectRatioChanged,
scaledBoundingBoxDimensionsReset,
} = canvasSlice.actions;
export default canvasSlice.reducer;

View File

@ -2,19 +2,22 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple';
import type { Dimensions } from 'features/canvas/store/canvasTypes';
import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants';
const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
const getScaledBoundingBoxDimensions = (
dimensions: Dimensions,
optimalDimension: number
) => {
const { width, height } = dimensions;
const scaledDimensions = { width, height };
const targetArea = 512 * 512;
const targetArea = optimalDimension * optimalDimension;
const aspectRatio = width / height;
let currentArea = width * height;
let maxDimension = 448;
let maxDimension = optimalDimension - CANVAS_GRID_SIZE_FINE;
while (currentArea < targetArea) {
maxDimension += CANVAS_GRID_SIZE_FINE;
if (width === height) {
scaledDimensions.width = 512;
scaledDimensions.height = 512;
scaledDimensions.width = optimalDimension;
scaledDimensions.height = optimalDimension;
break;
} else {
if (aspectRatio > 1) {