mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): modularize imagesize components
Canvas and non-canvas have separate width and height and need their own separate aspect ratios. In order to not duplicate a lot of aspect ratio logic, the components relating to image size have been modularized.
This commit is contained in:
parent
011757c497
commit
4f43eda09b
@ -1,23 +0,0 @@
|
|||||||
// When the aspect ratio is between these two values, we show the icon (experimentally determined)
|
|
||||||
export const ICON_LOW_CUTOFF = 0.23;
|
|
||||||
export const ICON_HIGH_CUTOFF = 1 / ICON_LOW_CUTOFF;
|
|
||||||
export const ICON_SIZE_PX = 48;
|
|
||||||
export const ICON_PADDING_PX = 16;
|
|
||||||
export const BOX_SIZE_CSS_CALC = `min(${ICON_SIZE_PX}px, calc(100% - ${ICON_PADDING_PX}px))`;
|
|
||||||
export const MOTION_ICON_INITIAL = {
|
|
||||||
opacity: 0,
|
|
||||||
};
|
|
||||||
export const MOTION_ICON_ANIMATE = {
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.1 },
|
|
||||||
};
|
|
||||||
export const MOTION_ICON_EXIT = {
|
|
||||||
opacity: 0,
|
|
||||||
transition: { duration: 0.1 },
|
|
||||||
};
|
|
||||||
export const ICON_CONTAINER_STYLES = {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
import type { IconType } from 'react-icons';
|
|
||||||
|
|
||||||
export type AspectRatioPreviewProps = {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
icon?: IconType;
|
|
||||||
};
|
|
@ -10,8 +10,7 @@ import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants';
|
|||||||
import floorCoordinates from 'features/canvas/util/floorCoordinates';
|
import floorCoordinates from 'features/canvas/util/floorCoordinates';
|
||||||
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
|
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
|
||||||
import roundDimensionsTo64 from 'features/canvas/util/roundDimensionsTo64';
|
import roundDimensionsTo64 from 'features/canvas/util/roundDimensionsTo64';
|
||||||
import { ASPECT_RATIO_MAP } from 'features/parameters/components/ImageSize/constants';
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
import { aspectRatioSelected } from 'features/parameters/store/generationSlice';
|
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { clamp, cloneDeep } from 'lodash-es';
|
import { clamp, cloneDeep } from 'lodash-es';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
import type { RgbaColor } from 'react-colorful';
|
||||||
@ -83,6 +82,11 @@ export const initialCanvasState: CanvasState = {
|
|||||||
stageScale: 1,
|
stageScale: 1,
|
||||||
tool: 'brush',
|
tool: 'brush',
|
||||||
batchIds: [],
|
batchIds: [],
|
||||||
|
aspectRatio: {
|
||||||
|
id: '1:1',
|
||||||
|
value: 1,
|
||||||
|
isLocked: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canvasSlice = createSlice({
|
export const canvasSlice = createSlice({
|
||||||
@ -198,8 +202,14 @@ export const canvasSlice = createSlice({
|
|||||||
state.stageScale = newScale;
|
state.stageScale = newScale;
|
||||||
state.stageCoordinates = newCoordinates;
|
state.stageCoordinates = newCoordinates;
|
||||||
},
|
},
|
||||||
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
|
setBoundingBoxDimensions: (
|
||||||
const newDimensions = roundDimensionsTo64(action.payload);
|
state,
|
||||||
|
action: PayloadAction<Partial<Dimensions>>
|
||||||
|
) => {
|
||||||
|
const newDimensions = roundDimensionsTo64({
|
||||||
|
...state.boundingBoxDimensions,
|
||||||
|
...action.payload,
|
||||||
|
});
|
||||||
state.boundingBoxDimensions = newDimensions;
|
state.boundingBoxDimensions = newDimensions;
|
||||||
|
|
||||||
if (state.boundingBoxScaleMethod === 'auto') {
|
if (state.boundingBoxScaleMethod === 'auto') {
|
||||||
@ -722,6 +732,21 @@ export const canvasSlice = createSlice({
|
|||||||
|
|
||||||
state.layerState.objects = [action.payload];
|
state.layerState.objects = [action.payload];
|
||||||
},
|
},
|
||||||
|
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
||||||
|
state.aspectRatio = action.payload;
|
||||||
|
// if (action.payload.id !== 'Free') {
|
||||||
|
// state.boundingBoxDimensions.height = roundToMultiple(
|
||||||
|
// state.boundingBoxDimensions.width /
|
||||||
|
// ASPECT_RATIO_MAP[action.payload.id].ratio,
|
||||||
|
// 64
|
||||||
|
// );
|
||||||
|
// state.scaledBoundingBoxDimensions.height = roundToMultiple(
|
||||||
|
// state.scaledBoundingBoxDimensions.width /
|
||||||
|
// ASPECT_RATIO_MAP[action.payload.id].ratio,
|
||||||
|
// 64
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(appSocketQueueItemStatusChanged, (state, action) => {
|
builder.addCase(appSocketQueueItemStatusChanged, (state, action) => {
|
||||||
@ -736,21 +761,6 @@ export const canvasSlice = createSlice({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.addCase(aspectRatioSelected, (state, action) => {
|
|
||||||
const aspectRatioID = action.payload;
|
|
||||||
if (aspectRatioID !== 'Free') {
|
|
||||||
state.boundingBoxDimensions.height = roundToMultiple(
|
|
||||||
state.boundingBoxDimensions.width /
|
|
||||||
ASPECT_RATIO_MAP[aspectRatioID].ratio,
|
|
||||||
64
|
|
||||||
);
|
|
||||||
state.scaledBoundingBoxDimensions.height = roundToMultiple(
|
|
||||||
state.scaledBoundingBoxDimensions.width /
|
|
||||||
ASPECT_RATIO_MAP[aspectRatioID].ratio,
|
|
||||||
64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.addMatcher(
|
builder.addMatcher(
|
||||||
queueApi.endpoints.clearQueue.matchFulfilled,
|
queueApi.endpoints.clearQueue.matchFulfilled,
|
||||||
(state) => {
|
(state) => {
|
||||||
@ -823,6 +833,7 @@ export const {
|
|||||||
canvasResized,
|
canvasResized,
|
||||||
canvasBatchIdAdded,
|
canvasBatchIdAdded,
|
||||||
canvasBatchIdsReset,
|
canvasBatchIdsReset,
|
||||||
|
aspectRatioChanged,
|
||||||
} = canvasSlice.actions;
|
} = canvasSlice.actions;
|
||||||
|
|
||||||
export default canvasSlice.reducer;
|
export default canvasSlice.reducer;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
import type { RgbaColor } from 'react-colorful';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@ -150,6 +151,7 @@ export interface CanvasState {
|
|||||||
tool: CanvasTool;
|
tool: CanvasTool;
|
||||||
generationMode?: GenerationMode;
|
generationMode?: GenerationMode;
|
||||||
batchIds: string[];
|
batchIds: string[];
|
||||||
|
aspectRatio: AspectRatioState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
|
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
|
||||||
|
@ -1,77 +1,45 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(
|
const selector = createMemoizedSelector(
|
||||||
[stateSelector, isStagingSelector],
|
[stateSelector, isStagingSelector],
|
||||||
({ canvas, generation }, isStaging) => {
|
({ generation }, isStaging) => {
|
||||||
const { boundingBoxDimensions } = canvas;
|
const { model } = generation;
|
||||||
const { model, aspectRatio } = generation;
|
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||||
|
model?.base_model as string
|
||||||
|
)
|
||||||
|
? 1024
|
||||||
|
: 512;
|
||||||
return {
|
return {
|
||||||
|
initial,
|
||||||
model,
|
model,
|
||||||
boundingBoxDimensions,
|
|
||||||
isStaging,
|
isStaging,
|
||||||
aspectRatio,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const ParamBoundingBoxWidth = () => {
|
const ParamBoundingBoxWidth = () => {
|
||||||
const dispatch = useAppDispatch();
|
const { isStaging, initial } = useAppSelector(selector);
|
||||||
const { model, boundingBoxDimensions, isStaging, aspectRatio } =
|
const ctx = useImageSizeContext();
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string)
|
const onChange = useCallback(
|
||||||
? 1024
|
|
||||||
: 512;
|
|
||||||
|
|
||||||
const handleChangeHeight = useCallback(
|
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(
|
ctx.heightChanged(v);
|
||||||
setBoundingBoxDimensions({
|
|
||||||
...boundingBoxDimensions,
|
|
||||||
height: Math.floor(v),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (aspectRatio) {
|
|
||||||
const newWidth = roundToMultiple(v * aspectRatio.value, 64);
|
|
||||||
dispatch(
|
|
||||||
setBoundingBoxDimensions({
|
|
||||||
width: newWidth,
|
|
||||||
height: Math.floor(v),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[aspectRatio, boundingBoxDimensions, dispatch]
|
[ctx]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResetHeight = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
dispatch(
|
ctx.heightChanged(initial);
|
||||||
setBoundingBoxDimensions({
|
}, [ctx, initial]);
|
||||||
...boundingBoxDimensions,
|
|
||||||
height: Math.floor(initial),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (aspectRatio) {
|
|
||||||
const newWidth = roundToMultiple(initial * aspectRatio.value, 64);
|
|
||||||
dispatch(
|
|
||||||
setBoundingBoxDimensions({
|
|
||||||
width: newWidth,
|
|
||||||
height: Math.floor(initial),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [aspectRatio, boundingBoxDimensions, dispatch, initial]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvControl label={t('parameters.height')} isDisabled={isStaging}>
|
<InvControl label={t('parameters.height')} isDisabled={isStaging}>
|
||||||
@ -79,9 +47,9 @@ const ParamBoundingBoxWidth = () => {
|
|||||||
min={64}
|
min={64}
|
||||||
max={1536}
|
max={1536}
|
||||||
step={64}
|
step={64}
|
||||||
value={boundingBoxDimensions.height}
|
value={ctx.height}
|
||||||
onChange={handleChangeHeight}
|
onChange={onChange}
|
||||||
onReset={handleResetHeight}
|
onReset={onReset}
|
||||||
marks
|
marks
|
||||||
withNumberInput
|
withNumberInput
|
||||||
numberInputMax={4096}
|
numberInputMax={4096}
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
// import { Flex, FormControl, FormLabel, Spacer } from '@chakra-ui/react';
|
|
||||||
// import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|
||||||
// import { stateSelector } from 'app/store/store';
|
|
||||||
// import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
// import { InvIconButton } from 'common/components';
|
|
||||||
// import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
|
|
||||||
// import { flipBoundingBoxAxes } from 'features/canvas/store/canvasSlice';
|
|
||||||
// import ParamAspectRatio, {
|
|
||||||
// mappedAspectRatios,
|
|
||||||
// } from 'features/parameters/components/Parameters/Core/ParamAspectRatio';
|
|
||||||
|
|
||||||
// import { useCallback } from 'react';
|
|
||||||
// import { useTranslation } from 'react-i18next';
|
|
||||||
// import { FaLock } from 'react-icons/fa';
|
|
||||||
// import { MdOutlineSwapVert } from 'react-icons/md';
|
|
||||||
// import ParamBoundingBoxHeight from './ParamBoundingBoxHeight';
|
|
||||||
// import ParamBoundingBoxWidth from './ParamBoundingBoxWidth';
|
|
||||||
|
|
||||||
// const sizeOptsSelector = createMemoizedSelector(
|
|
||||||
// [stateSelector],
|
|
||||||
// ({ generation, canvas }) => {
|
|
||||||
// const { shouldFitToWidthHeight, shouldLockAspectRatio } = generation;
|
|
||||||
// const { boundingBoxDimensions } = canvas;
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// shouldFitToWidthHeight,
|
|
||||||
// shouldLockAspectRatio,
|
|
||||||
// boundingBoxDimensions,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export default function ParamBoundingBoxSize() {
|
|
||||||
// const dispatch = useAppDispatch();
|
|
||||||
// const { t } = useTranslation();
|
|
||||||
|
|
||||||
// const { shouldLockAspectRatio, boundingBoxDimensions } =
|
|
||||||
// useAppSelector(sizeOptsSelector);
|
|
||||||
|
|
||||||
// const handleLockRatio = useCallback(() => {
|
|
||||||
// if (shouldLockAspectRatio) {
|
|
||||||
// dispatch(setShouldLockAspectRatio(false));
|
|
||||||
// if (
|
|
||||||
// !mappedAspectRatios.includes(
|
|
||||||
// boundingBoxDimensions.width / boundingBoxDimensions.height
|
|
||||||
// )
|
|
||||||
// ) {
|
|
||||||
// dispatch(setAspectRatio(null));
|
|
||||||
// } else {
|
|
||||||
// dispatch(
|
|
||||||
// setAspectRatio(
|
|
||||||
// boundingBoxDimensions.width / boundingBoxDimensions.height
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// dispatch(setShouldLockAspectRatio(true));
|
|
||||||
// dispatch(
|
|
||||||
// setAspectRatio(
|
|
||||||
// boundingBoxDimensions.width / boundingBoxDimensions.height
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }, [shouldLockAspectRatio, boundingBoxDimensions, dispatch]);
|
|
||||||
|
|
||||||
// const handleToggleSize = useCallback(() => {
|
|
||||||
// dispatch(flipBoundingBoxAxes());
|
|
||||||
// dispatch(setAspectRatio(null));
|
|
||||||
// if (shouldLockAspectRatio) {
|
|
||||||
// dispatch(
|
|
||||||
// setAspectRatio(
|
|
||||||
// boundingBoxDimensions.height / boundingBoxDimensions.width
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }, [dispatch, shouldLockAspectRatio, boundingBoxDimensions]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Flex
|
|
||||||
// sx={{
|
|
||||||
// gap: 2,
|
|
||||||
// p: 4,
|
|
||||||
// borderRadius: 4,
|
|
||||||
// flexDirection: 'column',
|
|
||||||
// w: 'full',
|
|
||||||
// bg: 'base.750',
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// <IAIInformationalPopover feature="paramRatio">
|
|
||||||
// <FormControl as={Flex} flexDir="row" alignItems="center" gap={2}>
|
|
||||||
// <FormLabel>{t('parameters.aspectRatio')}</FormLabel>
|
|
||||||
// <Spacer />
|
|
||||||
// <ParamAspectRatio />
|
|
||||||
// <InvIconButton
|
|
||||||
// tooltip={t('ui.swapSizes')}
|
|
||||||
// aria-label={t('ui.swapSizes')}
|
|
||||||
// size="sm"
|
|
||||||
// icon={<MdOutlineSwapVert />}
|
|
||||||
// fontSize={20}
|
|
||||||
// onClick={handleToggleSize}
|
|
||||||
// />
|
|
||||||
// <InvIconButton
|
|
||||||
// tooltip={t('ui.lockRatio')}
|
|
||||||
// aria-label={t('ui.lockRatio')}
|
|
||||||
// size="sm"
|
|
||||||
// icon={<FaLock />}
|
|
||||||
// isChecked={shouldLockAspectRatio}
|
|
||||||
// onClick={handleLockRatio}
|
|
||||||
// />
|
|
||||||
// </FormControl>
|
|
||||||
// </IAIInformationalPopover>
|
|
||||||
// <ParamBoundingBoxWidth />
|
|
||||||
// <ParamBoundingBoxHeight />
|
|
||||||
// </Flex>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
export default {};
|
|
@ -1,77 +1,45 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(
|
const selector = createMemoizedSelector(
|
||||||
[stateSelector, isStagingSelector],
|
[stateSelector, isStagingSelector],
|
||||||
({ canvas, generation }, isStaging) => {
|
({ generation }, isStaging) => {
|
||||||
const { boundingBoxDimensions } = canvas;
|
const { model } = generation;
|
||||||
const { model, aspectRatio } = generation;
|
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||||
|
model?.base_model as string
|
||||||
|
)
|
||||||
|
? 1024
|
||||||
|
: 512;
|
||||||
return {
|
return {
|
||||||
|
initial,
|
||||||
model,
|
model,
|
||||||
boundingBoxDimensions,
|
|
||||||
isStaging,
|
isStaging,
|
||||||
aspectRatio,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const ParamBoundingBoxWidth = () => {
|
const ParamBoundingBoxWidth = () => {
|
||||||
const dispatch = useAppDispatch();
|
const { isStaging, initial } = useAppSelector(selector);
|
||||||
const { model, boundingBoxDimensions, isStaging, aspectRatio } =
|
const ctx = useImageSizeContext();
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string)
|
|
||||||
? 1024
|
|
||||||
: 512;
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleChangeWidth = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(
|
ctx.widthChanged(v);
|
||||||
setBoundingBoxDimensions({
|
|
||||||
...boundingBoxDimensions,
|
|
||||||
width: Math.floor(v),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (aspectRatio) {
|
|
||||||
const newHeight = roundToMultiple(v / aspectRatio.value, 64);
|
|
||||||
dispatch(
|
|
||||||
setBoundingBoxDimensions({
|
|
||||||
width: Math.floor(v),
|
|
||||||
height: newHeight,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[aspectRatio, boundingBoxDimensions, dispatch]
|
[ctx]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResetWidth = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
dispatch(
|
ctx.widthChanged(initial);
|
||||||
setBoundingBoxDimensions({
|
}, [ctx, initial]);
|
||||||
...boundingBoxDimensions,
|
|
||||||
width: Math.floor(initial),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (aspectRatio) {
|
|
||||||
const newHeight = roundToMultiple(initial / aspectRatio.value, 64);
|
|
||||||
dispatch(
|
|
||||||
setBoundingBoxDimensions({
|
|
||||||
width: Math.floor(initial),
|
|
||||||
height: newHeight,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [aspectRatio, boundingBoxDimensions, dispatch, initial]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvControl label={t('parameters.width')} isDisabled={isStaging}>
|
<InvControl label={t('parameters.width')} isDisabled={isStaging}>
|
||||||
@ -79,9 +47,9 @@ const ParamBoundingBoxWidth = () => {
|
|||||||
min={64}
|
min={64}
|
||||||
max={1536}
|
max={1536}
|
||||||
step={64}
|
step={64}
|
||||||
value={boundingBoxDimensions.width}
|
value={ctx.width}
|
||||||
onChange={handleChangeWidth}
|
onChange={onChange}
|
||||||
onReset={handleResetWidth}
|
onReset={onReset}
|
||||||
withNumberInput
|
withNumberInput
|
||||||
numberInputMax={4096}
|
numberInputMax={4096}
|
||||||
marks
|
marks
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||||
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
||||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||||
import { heightChanged } from 'features/parameters/store/generationSlice';
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const selector = createMemoizedSelector(
|
|||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ generation, config }) => {
|
({ generation, config }) => {
|
||||||
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height;
|
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height;
|
||||||
const { model, height } = generation;
|
const { model } = generation;
|
||||||
|
|
||||||
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||||
model?.base_model as string
|
model?.base_model as string
|
||||||
@ -22,7 +22,6 @@ const selector = createMemoizedSelector(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
initial,
|
initial,
|
||||||
height,
|
|
||||||
min,
|
min,
|
||||||
max: sliderMax,
|
max: sliderMax,
|
||||||
inputMax,
|
inputMax,
|
||||||
@ -34,27 +33,27 @@ const selector = createMemoizedSelector(
|
|||||||
|
|
||||||
export const ParamHeight = memo(() => {
|
export const ParamHeight = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const ctx = useImageSizeContext();
|
||||||
const { initial, height, min, max, inputMax, step, fineStep } =
|
const { initial, min, max, inputMax, step, fineStep } =
|
||||||
useAppSelector(selector);
|
useAppSelector(selector);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(heightChanged(v));
|
ctx.heightChanged(v);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[ctx]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
dispatch(heightChanged(initial));
|
ctx.heightChanged(initial);
|
||||||
}, [dispatch, initial]);
|
}, [ctx, initial]);
|
||||||
|
|
||||||
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
|
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvControl label={t('parameters.height')}>
|
<InvControl label={t('parameters.height')}>
|
||||||
<InvSlider
|
<InvSlider
|
||||||
value={height}
|
value={ctx.height}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onReset={onReset}
|
onReset={onReset}
|
||||||
min={min}
|
min={min}
|
||||||
@ -64,7 +63,7 @@ export const ParamHeight = memo(() => {
|
|||||||
marks={marks}
|
marks={marks}
|
||||||
/>
|
/>
|
||||||
<InvNumberInput
|
<InvNumberInput
|
||||||
value={height}
|
value={ctx.height}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
min={min}
|
min={min}
|
||||||
max={inputMax}
|
max={inputMax}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||||
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
||||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||||
import { widthChanged } from 'features/parameters/store/generationSlice';
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const selector = createMemoizedSelector(
|
|||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ generation, config }) => {
|
({ generation, config }) => {
|
||||||
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width;
|
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width;
|
||||||
const { model, width } = generation;
|
const { model } = generation;
|
||||||
|
|
||||||
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||||
model?.base_model as string
|
model?.base_model as string
|
||||||
@ -22,7 +22,6 @@ const selector = createMemoizedSelector(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
initial,
|
initial,
|
||||||
width,
|
|
||||||
min,
|
min,
|
||||||
max: sliderMax,
|
max: sliderMax,
|
||||||
step: coarseStep,
|
step: coarseStep,
|
||||||
@ -33,27 +32,27 @@ const selector = createMemoizedSelector(
|
|||||||
);
|
);
|
||||||
export const ParamWidth = memo(() => {
|
export const ParamWidth = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const ctx = useImageSizeContext();
|
||||||
const { initial, width, min, max, inputMax, step, fineStep } =
|
const { initial, min, max, inputMax, step, fineStep } =
|
||||||
useAppSelector(selector);
|
useAppSelector(selector);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(widthChanged(v));
|
ctx.widthChanged(v);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[ctx]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
dispatch(widthChanged(initial));
|
ctx.widthChanged(initial);
|
||||||
}, [dispatch, initial]);
|
}, [ctx, initial]);
|
||||||
|
|
||||||
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
|
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvControl label={t('parameters.width')}>
|
<InvControl label={t('parameters.width')}>
|
||||||
<InvSlider
|
<InvSlider
|
||||||
value={width}
|
value={ctx.width}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onReset={onReset}
|
onReset={onReset}
|
||||||
min={min}
|
min={min}
|
||||||
@ -63,7 +62,7 @@ export const ParamWidth = memo(() => {
|
|||||||
marks={marks}
|
marks={marks}
|
||||||
/>
|
/>
|
||||||
<InvNumberInput
|
<InvNumberInput
|
||||||
value={width}
|
value={ctx.width}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
min={min}
|
min={min}
|
||||||
max={inputMax}
|
max={inputMax}
|
||||||
|
@ -1,29 +1,55 @@
|
|||||||
import { Flex, Icon } from '@chakra-ui/react';
|
import { Flex, Icon } from '@chakra-ui/react';
|
||||||
import { useSize } from '@chakra-ui/react-use-size';
|
import { useSize } from '@chakra-ui/react-use-size';
|
||||||
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { FaImage } from 'react-icons/fa';
|
import { FaImage } from 'react-icons/fa';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BOX_SIZE_CSS_CALC,
|
BOX_SIZE_CSS_CALC,
|
||||||
ICON_CONTAINER_STYLES,
|
ICON_CONTAINER_STYLES,
|
||||||
|
ICON_HIGH_CUTOFF,
|
||||||
|
ICON_LOW_CUTOFF,
|
||||||
MOTION_ICON_ANIMATE,
|
MOTION_ICON_ANIMATE,
|
||||||
MOTION_ICON_EXIT,
|
MOTION_ICON_EXIT,
|
||||||
MOTION_ICON_INITIAL,
|
MOTION_ICON_INITIAL,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { useAspectRatioPreviewState } from './hooks';
|
|
||||||
import type { AspectRatioPreviewProps } from './types';
|
|
||||||
|
|
||||||
export const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
|
export type AspectRatioPreviewProps = {
|
||||||
const { width: _width, height: _height, icon = FaImage } = props;
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AspectRatioPreview = () => {
|
||||||
|
const ctx = useImageSizeContext();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const containerSize = useSize(containerRef);
|
const containerSize = useSize(containerRef);
|
||||||
|
|
||||||
const { width, height, shouldShowIcon } = useAspectRatioPreviewState({
|
const shouldShowIcon = useMemo(
|
||||||
width: _width,
|
() =>
|
||||||
height: _height,
|
ctx.aspectRatioState.value < ICON_HIGH_CUTOFF &&
|
||||||
containerSize,
|
ctx.aspectRatioState.value > ICON_LOW_CUTOFF,
|
||||||
});
|
[ctx.aspectRatioState.value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { width, height } = useMemo(() => {
|
||||||
|
if (!containerSize) {
|
||||||
|
return { width: 0, height: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = ctx.width;
|
||||||
|
let height = ctx.height;
|
||||||
|
|
||||||
|
if (ctx.width > ctx.height) {
|
||||||
|
width = containerSize.width;
|
||||||
|
height = width / ctx.aspectRatioState.value;
|
||||||
|
} else {
|
||||||
|
height = containerSize.height;
|
||||||
|
width = height * ctx.aspectRatioState.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}, [containerSize, ctx.width, ctx.height, ctx.aspectRatioState.value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -50,7 +76,7 @@ export const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
|
|||||||
exit={MOTION_ICON_EXIT}
|
exit={MOTION_ICON_EXIT}
|
||||||
style={ICON_CONTAINER_STYLES}
|
style={ICON_CONTAINER_STYLES}
|
||||||
>
|
>
|
||||||
<Icon as={icon} color="base.700" boxSize={BOX_SIZE_CSS_CALC} />
|
<Icon as={FaImage} color="base.700" boxSize={BOX_SIZE_CSS_CALC} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
@ -1,12 +0,0 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { AspectRatioPreview } from 'common/components/AspectRatioPreview/AspectRatioPreview';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
export const AspectRatioPreviewWrapper = memo(() => {
|
|
||||||
const width = useAppSelector((state) => state.generation.width);
|
|
||||||
const height = useAppSelector((state) => state.generation.height);
|
|
||||||
|
|
||||||
return <AspectRatioPreview width={width} height={height} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
AspectRatioPreviewWrapper.displayName = 'AspectRatioPreviewWrapper';
|
|
@ -1,39 +1,34 @@
|
|||||||
import type { SystemStyleObject } from '@chakra-ui/styled-system';
|
import type { SystemStyleObject } from '@chakra-ui/styled-system';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import type { SingleValue } from 'chakra-react-select';
|
import type { SingleValue } from 'chakra-react-select';
|
||||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||||
import { InvSelect } from 'common/components/InvSelect/InvSelect';
|
import { InvSelect } from 'common/components/InvSelect/InvSelect';
|
||||||
import type { InvSelectOption } from 'common/components/InvSelect/types';
|
import type { InvSelectOption } from 'common/components/InvSelect/types';
|
||||||
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/ImageSize/constants';
|
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/ImageSize/constants';
|
||||||
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { isAspectRatioID } from 'features/parameters/components/ImageSize/types';
|
import { isAspectRatioID } from 'features/parameters/components/ImageSize/types';
|
||||||
import { aspectRatioSelected } from 'features/parameters/store/generationSlice';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { LockAspectRatioButton } from './LockAspectRatioButton';
|
|
||||||
import { SetOptimalSizeButton } from './SetOptimalSizeButton';
|
|
||||||
import { SwapDimensionsButton } from './SwapDimensionsButton';
|
|
||||||
|
|
||||||
export const AspectRatioSelect = memo(() => {
|
export const AspectRatioSelect = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const ctx = useImageSizeContext();
|
||||||
const aspectRatioID = useAppSelector(
|
|
||||||
(state) => state.generation.aspectRatio.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: SingleValue<InvSelectOption>) => {
|
(v: SingleValue<InvSelectOption>) => {
|
||||||
if (!v || !isAspectRatioID(v.value)) {
|
if (!v || !isAspectRatioID(v.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(aspectRatioSelected(v.value));
|
ctx.aspectRatioSelected(v.value);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[ctx]
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ASPECT_RATIO_OPTIONS.filter((o) => o.value === aspectRatioID)[0],
|
() =>
|
||||||
[aspectRatioID]
|
ASPECT_RATIO_OPTIONS.filter(
|
||||||
|
(o) => o.value === ctx.aspectRatioState.id
|
||||||
|
)[0],
|
||||||
|
[ctx.aspectRatioState.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -44,9 +39,6 @@ export const AspectRatioSelect = memo(() => {
|
|||||||
options={ASPECT_RATIO_OPTIONS}
|
options={ASPECT_RATIO_OPTIONS}
|
||||||
sx={selectStyles}
|
sx={selectStyles}
|
||||||
/>
|
/>
|
||||||
<SwapDimensionsButton />
|
|
||||||
<LockAspectRatioButton />
|
|
||||||
<SetOptimalSizeButton />
|
|
||||||
</InvControl>
|
</InvControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Flex } from '@chakra-ui/layout';
|
||||||
|
import { InvControlGroup } from 'common/components/InvControl/InvControlGroup';
|
||||||
|
import type { InvLabelProps } from 'common/components/InvControl/types';
|
||||||
|
import { AspectRatioPreview } from 'features/parameters/components/ImageSize/AspectRatioPreview';
|
||||||
|
import { AspectRatioSelect } from 'features/parameters/components/ImageSize/AspectRatioSelect';
|
||||||
|
import type { ImageSizeContextInnerValue } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
|
import { ImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
|
import { LockAspectRatioButton } from 'features/parameters/components/ImageSize/LockAspectRatioButton';
|
||||||
|
import { SetOptimalSizeButton } from 'features/parameters/components/ImageSize/SetOptimalSizeButton';
|
||||||
|
import { SwapDimensionsButton } from 'features/parameters/components/ImageSize/SwapDimensionsButton';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
type ImageSizeProps = ImageSizeContextInnerValue & {
|
||||||
|
widthComponent: ReactNode;
|
||||||
|
heightComponent: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageSize = memo((props: ImageSizeProps) => {
|
||||||
|
const { widthComponent, heightComponent, ...ctx } = props;
|
||||||
|
return (
|
||||||
|
<ImageSizeContext.Provider value={ctx}>
|
||||||
|
<Flex gap={4} alignItems="center">
|
||||||
|
<Flex gap={4} flexDirection="column" width="full">
|
||||||
|
<InvControlGroup labelProps={labelProps}>
|
||||||
|
<Flex gap={4}>
|
||||||
|
<AspectRatioSelect />
|
||||||
|
<SwapDimensionsButton />
|
||||||
|
<LockAspectRatioButton />
|
||||||
|
<SetOptimalSizeButton />
|
||||||
|
</Flex>
|
||||||
|
{widthComponent}
|
||||||
|
{heightComponent}
|
||||||
|
</InvControlGroup>
|
||||||
|
</Flex>
|
||||||
|
<Flex w="98px" h="98px" flexShrink={0} flexGrow={0}>
|
||||||
|
<AspectRatioPreview />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</ImageSizeContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageSize.displayName = 'ImageSize';
|
||||||
|
|
||||||
|
const labelProps: InvLabelProps = {
|
||||||
|
minW: 14,
|
||||||
|
};
|
@ -0,0 +1,176 @@
|
|||||||
|
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
|
import {
|
||||||
|
ASPECT_RATIO_MAP,
|
||||||
|
initialAspectRatioState,
|
||||||
|
} from 'features/parameters/components/ImageSize/constants';
|
||||||
|
import type {
|
||||||
|
AspectRatioID,
|
||||||
|
AspectRatioState,
|
||||||
|
} from 'features/parameters/components/ImageSize/types';
|
||||||
|
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
export type ImageSizeContextInnerValue = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
aspectRatioState: AspectRatioState;
|
||||||
|
onChangeWidth: (width: number) => void;
|
||||||
|
onChangeHeight: (height: number) => void;
|
||||||
|
onChangeAspectRatioState: (aspectRatioState: AspectRatioState) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageSizeContext = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
aspectRatioState: AspectRatioState;
|
||||||
|
aspectRatioSelected: (aspectRatioID: AspectRatioID) => void;
|
||||||
|
dimensionsSwapped: () => void;
|
||||||
|
widthChanged: (width: number) => void;
|
||||||
|
heightChanged: (height: number) => void;
|
||||||
|
isLockedToggled: () => void;
|
||||||
|
sizeReset: (width: number, height: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageSizeContext =
|
||||||
|
createContext<ImageSizeContextInnerValue | null>(null);
|
||||||
|
|
||||||
|
export const useImageSizeContext = (): ImageSizeContext => {
|
||||||
|
const _ctx = useContext(ImageSizeContext);
|
||||||
|
|
||||||
|
if (!_ctx) {
|
||||||
|
throw new Error(
|
||||||
|
'useImageSizeContext must be used within a ImageSizeContext.Provider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aspectRatioSelected = useCallback(
|
||||||
|
(aspectRatioID: AspectRatioID) => {
|
||||||
|
const state: AspectRatioState = {
|
||||||
|
..._ctx.aspectRatioState,
|
||||||
|
id: aspectRatioID,
|
||||||
|
};
|
||||||
|
if (state.id === 'Free') {
|
||||||
|
// If the new aspect ratio is free, we only unlock
|
||||||
|
state.isLocked = false;
|
||||||
|
} else {
|
||||||
|
// The new aspect ratio not free, so we need to coerce the size & lock
|
||||||
|
state.isLocked = true;
|
||||||
|
state.value = ASPECT_RATIO_MAP[state.id].ratio;
|
||||||
|
const { width, height } = calculateNewSize(
|
||||||
|
state.value,
|
||||||
|
_ctx.width,
|
||||||
|
_ctx.height
|
||||||
|
);
|
||||||
|
_ctx.onChangeWidth(width);
|
||||||
|
_ctx.onChangeHeight(height);
|
||||||
|
}
|
||||||
|
_ctx.onChangeAspectRatioState(state);
|
||||||
|
},
|
||||||
|
[_ctx]
|
||||||
|
);
|
||||||
|
const dimensionsSwapped = useCallback(() => {
|
||||||
|
const state = {
|
||||||
|
..._ctx.aspectRatioState,
|
||||||
|
};
|
||||||
|
// We always invert the aspect ratio
|
||||||
|
state.value = 1 / state.value;
|
||||||
|
if (state.id === 'Free') {
|
||||||
|
// If the aspect ratio is free, we just swap the dimensions
|
||||||
|
const newWidth = _ctx.height;
|
||||||
|
const newHeight = _ctx.width;
|
||||||
|
_ctx.onChangeWidth(newWidth);
|
||||||
|
_ctx.onChangeHeight(newHeight);
|
||||||
|
} else {
|
||||||
|
// Else we need to calculate the new size
|
||||||
|
const { width, height } = calculateNewSize(
|
||||||
|
state.value,
|
||||||
|
_ctx.width,
|
||||||
|
_ctx.height
|
||||||
|
);
|
||||||
|
_ctx.onChangeWidth(width);
|
||||||
|
_ctx.onChangeHeight(height);
|
||||||
|
// Update the aspect ratio ID to match the new aspect ratio
|
||||||
|
state.id = ASPECT_RATIO_MAP[state.id].inverseID;
|
||||||
|
}
|
||||||
|
_ctx.onChangeAspectRatioState(state);
|
||||||
|
}, [_ctx]);
|
||||||
|
|
||||||
|
const widthChanged = useCallback(
|
||||||
|
(width: number) => {
|
||||||
|
let height = _ctx.height;
|
||||||
|
const state = { ..._ctx.aspectRatioState };
|
||||||
|
if (state.isLocked) {
|
||||||
|
// When locked, we calculate the new height based on the aspect ratio
|
||||||
|
height = roundToMultiple(width / state.value, 8);
|
||||||
|
} else {
|
||||||
|
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
||||||
|
state.isLocked = false;
|
||||||
|
state.id = 'Free';
|
||||||
|
state.value = width / height;
|
||||||
|
}
|
||||||
|
_ctx.onChangeWidth(width);
|
||||||
|
_ctx.onChangeHeight(height);
|
||||||
|
_ctx.onChangeAspectRatioState(state);
|
||||||
|
},
|
||||||
|
[_ctx]
|
||||||
|
);
|
||||||
|
|
||||||
|
const heightChanged = useCallback(
|
||||||
|
(height: number) => {
|
||||||
|
let width = _ctx.width;
|
||||||
|
const state = { ..._ctx.aspectRatioState };
|
||||||
|
if (state.isLocked) {
|
||||||
|
// When locked, we calculate the new width based on the aspect ratio
|
||||||
|
width = roundToMultiple(height * state.value, 8);
|
||||||
|
} else {
|
||||||
|
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
||||||
|
state.isLocked = false;
|
||||||
|
state.id = 'Free';
|
||||||
|
state.value = width / height;
|
||||||
|
}
|
||||||
|
_ctx.onChangeWidth(width);
|
||||||
|
_ctx.onChangeHeight(height);
|
||||||
|
_ctx.onChangeAspectRatioState(state);
|
||||||
|
},
|
||||||
|
[_ctx]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLockedToggled = useCallback(() => {
|
||||||
|
const state = { ..._ctx.aspectRatioState };
|
||||||
|
state.isLocked = !state.isLocked;
|
||||||
|
_ctx.onChangeAspectRatioState(state);
|
||||||
|
}, [_ctx]);
|
||||||
|
|
||||||
|
const sizeReset = useCallback(
|
||||||
|
(width: number, height: number) => {
|
||||||
|
const state = { ...initialAspectRatioState };
|
||||||
|
_ctx.onChangeAspectRatioState(state);
|
||||||
|
_ctx.onChangeWidth(width);
|
||||||
|
_ctx.onChangeHeight(height);
|
||||||
|
},
|
||||||
|
[_ctx]
|
||||||
|
);
|
||||||
|
|
||||||
|
const ctx = useMemo(
|
||||||
|
() => ({
|
||||||
|
..._ctx,
|
||||||
|
aspectRatioSelected,
|
||||||
|
dimensionsSwapped,
|
||||||
|
widthChanged,
|
||||||
|
heightChanged,
|
||||||
|
isLockedToggled,
|
||||||
|
sizeReset,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
_ctx,
|
||||||
|
aspectRatioSelected,
|
||||||
|
dimensionsSwapped,
|
||||||
|
heightChanged,
|
||||||
|
isLockedToggled,
|
||||||
|
sizeReset,
|
||||||
|
widthChanged,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
@ -1,27 +1,23 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||||
import { isLockedToggled } from 'features/parameters/store/generationSlice';
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaLock, FaLockOpen } from 'react-icons/fa6';
|
import { FaLock, FaLockOpen } from 'react-icons/fa6';
|
||||||
|
|
||||||
export const LockAspectRatioButton = memo(() => {
|
export const LockAspectRatioButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const ctx = useImageSizeContext();
|
||||||
const isLocked = useAppSelector(
|
|
||||||
(state) => state.generation.aspectRatio.isLocked
|
|
||||||
);
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(isLockedToggled());
|
ctx.isLockedToggled();
|
||||||
}, [dispatch]);
|
}, [ctx]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvIconButton
|
<InvIconButton
|
||||||
aria-label={t('parameters.lockAspectRatio')}
|
aria-label={t('parameters.lockAspectRatio')}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={isLocked ? 'outline' : 'ghost'}
|
variant={ctx.aspectRatioState.isLocked ? 'outline' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={isLocked ? <FaLock /> : <FaLockOpen />}
|
icon={ctx.aspectRatioState.isLocked ? <FaLock /> : <FaLockOpen />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||||
import { sizeReset } from 'features/parameters/store/generationSlice';
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IoSparkles } from 'react-icons/io5';
|
import { IoSparkles } from 'react-icons/io5';
|
||||||
|
|
||||||
export const SetOptimalSizeButton = memo(() => {
|
export const SetOptimalSizeButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const ctx = useImageSizeContext();
|
||||||
const optimalDimension = useAppSelector((state) =>
|
const optimalDimension = useAppSelector((state) =>
|
||||||
state.generation.model?.base_model === 'sdxl' ? 1024 : 512
|
state.generation.model?.base_model === 'sdxl' ? 1024 : 512
|
||||||
);
|
);
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(sizeReset(optimalDimension));
|
ctx.sizeReset(optimalDimension, optimalDimension);
|
||||||
}, [dispatch, optimalDimension]);
|
}, [ctx, optimalDimension]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvIconButton
|
<InvIconButton
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||||
import { dimensionsSwapped } from 'features/parameters/store/generationSlice';
|
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IoSwapVertical } from 'react-icons/io5';
|
import { IoSwapVertical } from 'react-icons/io5';
|
||||||
|
|
||||||
export const SwapDimensionsButton = memo(() => {
|
export const SwapDimensionsButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const ctx = useImageSizeContext();
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(dimensionsSwapped());
|
ctx.dimensionsSwapped();
|
||||||
}, [dispatch]);
|
}, [ctx]);
|
||||||
return (
|
return (
|
||||||
<InvIconButton
|
<InvIconButton
|
||||||
aria-label={t('parameters.swapDimensions')}
|
aria-label={t('parameters.swapDimensions')}
|
||||||
|
@ -1,6 +1,30 @@
|
|||||||
import type { InvSelectOption } from 'common/components/InvSelect/types';
|
import type { InvSelectOption } from 'common/components/InvSelect/types';
|
||||||
|
|
||||||
import type { AspectRatioID } from './types';
|
import type { AspectRatioID, AspectRatioState } from './types';
|
||||||
|
|
||||||
|
// When the aspect ratio is between these two values, we show the icon (experimentally determined)
|
||||||
|
export const ICON_LOW_CUTOFF = 0.23;
|
||||||
|
export const ICON_HIGH_CUTOFF = 1 / ICON_LOW_CUTOFF;
|
||||||
|
export const ICON_SIZE_PX = 48;
|
||||||
|
export const ICON_PADDING_PX = 16;
|
||||||
|
export const BOX_SIZE_CSS_CALC = `min(${ICON_SIZE_PX}px, calc(100% - ${ICON_PADDING_PX}px))`;
|
||||||
|
export const MOTION_ICON_INITIAL = {
|
||||||
|
opacity: 0,
|
||||||
|
};
|
||||||
|
export const MOTION_ICON_ANIMATE = {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
};
|
||||||
|
export const MOTION_ICON_EXIT = {
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
};
|
||||||
|
export const ICON_CONTAINER_STYLES = {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
export const ASPECT_RATIO_OPTIONS: InvSelectOption[] = [
|
export const ASPECT_RATIO_OPTIONS: InvSelectOption[] = [
|
||||||
{ label: 'Free' as const, value: 'Free' },
|
{ label: 'Free' as const, value: 'Free' },
|
||||||
@ -25,3 +49,9 @@ export const ASPECT_RATIO_MAP: Record<
|
|||||||
'2:3': { ratio: 2 / 3, inverseID: '3:2' },
|
'2:3': { ratio: 2 / 3, inverseID: '3:2' },
|
||||||
'9:16': { ratio: 9 / 16, inverseID: '16:9' },
|
'9:16': { ratio: 9 / 16, inverseID: '16:9' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialAspectRatioState: AspectRatioState = {
|
||||||
|
id: '1:1',
|
||||||
|
value: 1,
|
||||||
|
isLocked: false,
|
||||||
|
};
|
||||||
|
@ -13,3 +13,9 @@ export const zAspectRatioID = z.enum([
|
|||||||
export type AspectRatioID = z.infer<typeof zAspectRatioID>;
|
export type AspectRatioID = z.infer<typeof zAspectRatioID>;
|
||||||
export const isAspectRatioID = (v: string): v is AspectRatioID =>
|
export const isAspectRatioID = (v: string): v is AspectRatioID =>
|
||||||
zAspectRatioID.safeParse(v).success;
|
zAspectRatioID.safeParse(v).success;
|
||||||
|
|
||||||
|
export type AspectRatioState = {
|
||||||
|
id: AspectRatioID;
|
||||||
|
value: number;
|
||||||
|
isLocked: boolean;
|
||||||
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { GenerationState } from './generationSlice';
|
import type { GenerationState } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generation slice persist denylist
|
* Generation slice persist denylist
|
||||||
|
@ -2,77 +2,25 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
|
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
|
||||||
import { ASPECT_RATIO_MAP } from 'features/parameters/components/ImageSize/constants';
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
import type { AspectRatioID } from 'features/parameters/components/ImageSize/types';
|
|
||||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||||
import type {
|
import type {
|
||||||
ParameterCanvasCoherenceMode,
|
ParameterCanvasCoherenceMode,
|
||||||
ParameterCFGRescaleMultiplier,
|
ParameterCFGRescaleMultiplier,
|
||||||
ParameterCFGScale,
|
ParameterCFGScale,
|
||||||
ParameterHeight,
|
|
||||||
ParameterMaskBlurMethod,
|
ParameterMaskBlurMethod,
|
||||||
ParameterModel,
|
ParameterModel,
|
||||||
ParameterNegativePrompt,
|
|
||||||
ParameterPositivePrompt,
|
|
||||||
ParameterPrecision,
|
ParameterPrecision,
|
||||||
ParameterScheduler,
|
ParameterScheduler,
|
||||||
ParameterSeed,
|
|
||||||
ParameterSteps,
|
|
||||||
ParameterStrength,
|
|
||||||
ParameterVAEModel,
|
ParameterVAEModel,
|
||||||
ParameterWidth,
|
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
|
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { clamp, cloneDeep } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
export interface GenerationState {
|
import type { GenerationState } from './types';
|
||||||
cfgScale: ParameterCFGScale;
|
|
||||||
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
|
|
||||||
height: ParameterHeight;
|
|
||||||
img2imgStrength: ParameterStrength;
|
|
||||||
infillMethod: string;
|
|
||||||
initialImage?: { imageName: string; width: number; height: number };
|
|
||||||
iterations: number;
|
|
||||||
perlin: number;
|
|
||||||
positivePrompt: ParameterPositivePrompt;
|
|
||||||
negativePrompt: ParameterNegativePrompt;
|
|
||||||
scheduler: ParameterScheduler;
|
|
||||||
maskBlur: number;
|
|
||||||
maskBlurMethod: ParameterMaskBlurMethod;
|
|
||||||
canvasCoherenceMode: ParameterCanvasCoherenceMode;
|
|
||||||
canvasCoherenceSteps: number;
|
|
||||||
canvasCoherenceStrength: ParameterStrength;
|
|
||||||
seed: ParameterSeed;
|
|
||||||
seedWeights: string;
|
|
||||||
shouldFitToWidthHeight: boolean;
|
|
||||||
shouldGenerateVariations: boolean;
|
|
||||||
shouldRandomizeSeed: boolean;
|
|
||||||
steps: ParameterSteps;
|
|
||||||
threshold: number;
|
|
||||||
infillTileSize: number;
|
|
||||||
infillPatchmatchDownscaleSize: number;
|
|
||||||
variationAmount: number;
|
|
||||||
width: ParameterWidth;
|
|
||||||
shouldUseSymmetry: boolean;
|
|
||||||
horizontalSymmetrySteps: number;
|
|
||||||
verticalSymmetrySteps: number;
|
|
||||||
model: ParameterModel | null;
|
|
||||||
vae: ParameterVAEModel | null;
|
|
||||||
vaePrecision: ParameterPrecision;
|
|
||||||
seamlessXAxis: boolean;
|
|
||||||
seamlessYAxis: boolean;
|
|
||||||
clipSkip: number;
|
|
||||||
shouldUseCpuNoise: boolean;
|
|
||||||
shouldShowAdvancedOptions: boolean;
|
|
||||||
aspectRatio: {
|
|
||||||
id: AspectRatioID;
|
|
||||||
value: number;
|
|
||||||
isLocked: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialGenerationState: GenerationState = {
|
export const initialGenerationState: GenerationState = {
|
||||||
cfgScale: 7.5,
|
cfgScale: 7.5,
|
||||||
@ -112,18 +60,12 @@ export const initialGenerationState: GenerationState = {
|
|||||||
clipSkip: 0,
|
clipSkip: 0,
|
||||||
shouldUseCpuNoise: true,
|
shouldUseCpuNoise: true,
|
||||||
shouldShowAdvancedOptions: false,
|
shouldShowAdvancedOptions: false,
|
||||||
aspectRatio: {
|
aspectRatio: { ...initialAspectRatioState },
|
||||||
id: '1:1',
|
|
||||||
value: 1,
|
|
||||||
isLocked: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: GenerationState = initialGenerationState;
|
|
||||||
|
|
||||||
export const generationSlice = createSlice({
|
export const generationSlice = createSlice({
|
||||||
name: 'generation',
|
name: 'generation',
|
||||||
initialState,
|
initialState: initialGenerationState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setPositivePrompt: (state, action: PayloadAction<string>) => {
|
setPositivePrompt: (state, action: PayloadAction<string>) => {
|
||||||
state.positivePrompt = action.payload;
|
state.positivePrompt = action.payload;
|
||||||
@ -279,93 +221,15 @@ export const generationSlice = createSlice({
|
|||||||
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
|
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldUseCpuNoise = action.payload;
|
state.shouldUseCpuNoise = action.payload;
|
||||||
},
|
},
|
||||||
aspectRatioSelected: (
|
widthChanged: (state, action: PayloadAction<number>) => {
|
||||||
state,
|
|
||||||
action: PayloadAction<GenerationState['aspectRatio']['id']>
|
|
||||||
) => {
|
|
||||||
const aspectRatioID = action.payload;
|
|
||||||
state.aspectRatio.id = aspectRatioID;
|
|
||||||
if (aspectRatioID === 'Free') {
|
|
||||||
// If the new aspect ratio is free, we only unlock
|
|
||||||
state.aspectRatio.isLocked = false;
|
|
||||||
} else {
|
|
||||||
// The new aspect ratio not free, so we need to coerce the size & lock
|
|
||||||
state.aspectRatio.isLocked = true;
|
|
||||||
const aspectRatio = ASPECT_RATIO_MAP[aspectRatioID].ratio;
|
|
||||||
state.aspectRatio.value = aspectRatio;
|
|
||||||
const { width, height } = calculateNewSize(
|
|
||||||
aspectRatio,
|
|
||||||
state.width,
|
|
||||||
state.height
|
|
||||||
);
|
|
||||||
state.width = width;
|
|
||||||
state.height = height;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dimensionsSwapped: (state) => {
|
|
||||||
// We always invert the aspect ratio
|
|
||||||
const aspectRatio = 1 / state.aspectRatio.value;
|
|
||||||
state.aspectRatio.value = aspectRatio;
|
|
||||||
if (state.aspectRatio.id === 'Free') {
|
|
||||||
// If the aspect ratio is free, we just swap the dimensions
|
|
||||||
const oldWidth = state.width;
|
|
||||||
const oldHeight = state.height;
|
|
||||||
state.width = oldHeight;
|
|
||||||
state.height = oldWidth;
|
|
||||||
} else {
|
|
||||||
// Else we need to calculate the new size
|
|
||||||
const { width, height } = calculateNewSize(
|
|
||||||
aspectRatio,
|
|
||||||
state.width,
|
|
||||||
state.height
|
|
||||||
);
|
|
||||||
state.width = width;
|
|
||||||
state.height = height;
|
|
||||||
// Update the aspect ratio ID to match the new aspect ratio
|
|
||||||
state.aspectRatio.id = ASPECT_RATIO_MAP[state.aspectRatio.id].inverseID;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
widthChanged: (state, action: PayloadAction<GenerationState['width']>) => {
|
|
||||||
const width = action.payload;
|
|
||||||
let height = state.height;
|
|
||||||
if (state.aspectRatio.isLocked) {
|
|
||||||
// When locked, we calculate the new height based on the aspect ratio
|
|
||||||
height = roundToMultiple(width / state.aspectRatio.value, 8);
|
|
||||||
} else {
|
|
||||||
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
|
||||||
state.aspectRatio.isLocked = false;
|
|
||||||
state.aspectRatio.id = 'Free';
|
|
||||||
state.aspectRatio.value = width / height;
|
|
||||||
}
|
|
||||||
state.width = width;
|
|
||||||
state.height = height;
|
|
||||||
},
|
|
||||||
heightChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<GenerationState['height']>
|
|
||||||
) => {
|
|
||||||
const height = action.payload;
|
|
||||||
let width = state.width;
|
|
||||||
if (state.aspectRatio.isLocked) {
|
|
||||||
// When locked, we calculate the new width based on the aspect ratio
|
|
||||||
width = roundToMultiple(height * state.aspectRatio.value, 8);
|
|
||||||
} else {
|
|
||||||
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
|
||||||
state.aspectRatio.isLocked = false;
|
|
||||||
state.aspectRatio.id = 'Free';
|
|
||||||
state.aspectRatio.value = width / height;
|
|
||||||
}
|
|
||||||
state.height = height;
|
|
||||||
state.width = width;
|
|
||||||
},
|
|
||||||
isLockedToggled: (state) => {
|
|
||||||
state.aspectRatio.isLocked = !state.aspectRatio.isLocked;
|
|
||||||
},
|
|
||||||
sizeReset: (state, action: PayloadAction<number>) => {
|
|
||||||
state.aspectRatio = cloneDeep(initialGenerationState.aspectRatio);
|
|
||||||
state.width = action.payload;
|
state.width = action.payload;
|
||||||
|
},
|
||||||
|
heightChanged: (state, action: PayloadAction<number>) => {
|
||||||
state.height = action.payload;
|
state.height = action.payload;
|
||||||
},
|
},
|
||||||
|
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
||||||
|
state.aspectRatio = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(configChanged, (state, action) => {
|
builder.addCase(configChanged, (state, action) => {
|
||||||
@ -436,13 +300,10 @@ export const {
|
|||||||
setSeamlessYAxis,
|
setSeamlessYAxis,
|
||||||
setClipSkip,
|
setClipSkip,
|
||||||
shouldUseCpuNoiseChanged,
|
shouldUseCpuNoiseChanged,
|
||||||
aspectRatioSelected,
|
vaePrecisionChanged,
|
||||||
dimensionsSwapped,
|
aspectRatioChanged,
|
||||||
widthChanged,
|
widthChanged,
|
||||||
heightChanged,
|
heightChanged,
|
||||||
isLockedToggled,
|
|
||||||
sizeReset,
|
|
||||||
vaePrecisionChanged,
|
|
||||||
} = generationSlice.actions;
|
} = generationSlice.actions;
|
||||||
|
|
||||||
export default generationSlice.reducer;
|
export default generationSlice.reducer;
|
||||||
|
60
invokeai/frontend/web/src/features/parameters/store/types.ts
Normal file
60
invokeai/frontend/web/src/features/parameters/store/types.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
|
import type {
|
||||||
|
ParameterCanvasCoherenceMode,
|
||||||
|
ParameterCFGRescaleMultiplier,
|
||||||
|
ParameterCFGScale,
|
||||||
|
ParameterHeight,
|
||||||
|
ParameterMaskBlurMethod,
|
||||||
|
ParameterModel,
|
||||||
|
ParameterNegativePrompt,
|
||||||
|
ParameterPositivePrompt,
|
||||||
|
ParameterPrecision,
|
||||||
|
ParameterScheduler,
|
||||||
|
ParameterSeed,
|
||||||
|
ParameterSteps,
|
||||||
|
ParameterStrength,
|
||||||
|
ParameterVAEModel,
|
||||||
|
ParameterWidth,
|
||||||
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
|
|
||||||
|
export interface GenerationState {
|
||||||
|
cfgScale: ParameterCFGScale;
|
||||||
|
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
|
||||||
|
height: ParameterHeight;
|
||||||
|
img2imgStrength: ParameterStrength;
|
||||||
|
infillMethod: string;
|
||||||
|
initialImage?: { imageName: string; width: number; height: number };
|
||||||
|
iterations: number;
|
||||||
|
perlin: number;
|
||||||
|
positivePrompt: ParameterPositivePrompt;
|
||||||
|
negativePrompt: ParameterNegativePrompt;
|
||||||
|
scheduler: ParameterScheduler;
|
||||||
|
maskBlur: number;
|
||||||
|
maskBlurMethod: ParameterMaskBlurMethod;
|
||||||
|
canvasCoherenceMode: ParameterCanvasCoherenceMode;
|
||||||
|
canvasCoherenceSteps: number;
|
||||||
|
canvasCoherenceStrength: ParameterStrength;
|
||||||
|
seed: ParameterSeed;
|
||||||
|
seedWeights: string;
|
||||||
|
shouldFitToWidthHeight: boolean;
|
||||||
|
shouldGenerateVariations: boolean;
|
||||||
|
shouldRandomizeSeed: boolean;
|
||||||
|
steps: ParameterSteps;
|
||||||
|
threshold: number;
|
||||||
|
infillTileSize: number;
|
||||||
|
infillPatchmatchDownscaleSize: number;
|
||||||
|
variationAmount: number;
|
||||||
|
width: ParameterWidth;
|
||||||
|
shouldUseSymmetry: boolean;
|
||||||
|
horizontalSymmetrySteps: number;
|
||||||
|
verticalSymmetrySteps: number;
|
||||||
|
model: ParameterModel | null;
|
||||||
|
vae: ParameterVAEModel | null;
|
||||||
|
vaePrecision: ParameterPrecision;
|
||||||
|
seamlessXAxis: boolean;
|
||||||
|
seamlessYAxis: boolean;
|
||||||
|
clipSkip: number;
|
||||||
|
shouldUseCpuNoise: boolean;
|
||||||
|
shouldShowAdvancedOptions: boolean;
|
||||||
|
aspectRatio: AspectRatioState;
|
||||||
|
}
|
@ -7,25 +7,21 @@ import type { InvLabelProps } from 'common/components/InvControl/types';
|
|||||||
import { InvExpander } from 'common/components/InvExpander/InvExpander';
|
import { InvExpander } from 'common/components/InvExpander/InvExpander';
|
||||||
import { InvSingleAccordion } from 'common/components/InvSingleAccordion/InvSingleAccordion';
|
import { InvSingleAccordion } from 'common/components/InvSingleAccordion/InvSingleAccordion';
|
||||||
import { HrfSettings } from 'features/hrf/components/HrfSettings';
|
import { HrfSettings } from 'features/hrf/components/HrfSettings';
|
||||||
import ParamBoundingBoxHeight from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight';
|
|
||||||
import ParamBoundingBoxWidth from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth';
|
|
||||||
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
|
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
|
||||||
import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
|
import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
|
||||||
import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth';
|
import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth';
|
||||||
import { ParamHeight } from 'features/parameters/components/Core/ParamHeight';
|
|
||||||
import { ParamWidth } from 'features/parameters/components/Core/ParamWidth';
|
|
||||||
import { AspectRatioPreviewWrapper } from 'features/parameters/components/ImageSize/AspectRatioPreviewWrapper';
|
|
||||||
import { AspectRatioSelect } from 'features/parameters/components/ImageSize/AspectRatioSelect';
|
|
||||||
import ImageToImageFit from 'features/parameters/components/ImageToImage/ImageToImageFit';
|
import ImageToImageFit from 'features/parameters/components/ImageToImage/ImageToImageFit';
|
||||||
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
||||||
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
|
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
|
||||||
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
|
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
|
||||||
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
|
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { ImageSizeCanvas } from './ImageSizeCanvas';
|
||||||
|
import { ImageSizeLinear } from './ImageSizeLinear';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(
|
const selector = createMemoizedSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
({ generation, hrf }, activeTabName) => {
|
({ generation, hrf }, activeTabName) => {
|
||||||
@ -42,10 +38,6 @@ const selector = createMemoizedSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const labelProps: InvLabelProps = {
|
|
||||||
minW: 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
const scalingLabelProps: InvLabelProps = {
|
const scalingLabelProps: InvLabelProps = {
|
||||||
minW: '4.5rem',
|
minW: '4.5rem',
|
||||||
};
|
};
|
||||||
@ -61,17 +53,11 @@ export const ImageSettingsAccordion = memo(() => {
|
|||||||
badges={badges}
|
badges={badges}
|
||||||
>
|
>
|
||||||
<Flex px={4} pt={4} w="full" h="full" flexDir="column">
|
<Flex px={4} pt={4} w="full" h="full" flexDir="column">
|
||||||
<Flex gap={4} alignItems="center">
|
{activeTabName === 'unifiedCanvas' ? (
|
||||||
<Flex gap={4} flexDirection="column" width="full">
|
<ImageSizeCanvas />
|
||||||
<InvControlGroup labelProps={labelProps}>
|
) : (
|
||||||
<AspectRatioSelect />
|
<ImageSizeLinear />
|
||||||
<WidthHeight activeTabName={activeTabName} />
|
)}
|
||||||
</InvControlGroup>
|
|
||||||
</Flex>
|
|
||||||
<Flex w="98px" h="98px" flexShrink={0} flexGrow={0}>
|
|
||||||
<AspectRatioPreviewWrapper />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
<InvExpander>
|
<InvExpander>
|
||||||
<Flex gap={4} pb={4} flexDir="column">
|
<Flex gap={4} pb={4} flexDir="column">
|
||||||
<Flex gap={4}>
|
<Flex gap={4}>
|
||||||
@ -79,10 +65,10 @@ export const ImageSettingsAccordion = memo(() => {
|
|||||||
<ParamSeedShuffle />
|
<ParamSeedShuffle />
|
||||||
<ParamSeedRandomize />
|
<ParamSeedRandomize />
|
||||||
</Flex>
|
</Flex>
|
||||||
{activeTabName === 'txt2img' && <HrfSettings />}
|
|
||||||
{activeTabName === 'img2img' && <ImageToImageFit />}
|
{activeTabName === 'img2img' && <ImageToImageFit />}
|
||||||
{(activeTabName === 'img2img' ||
|
{(activeTabName === 'img2img' ||
|
||||||
activeTabName === 'unifiedCanvas') && <ImageToImageStrength />}
|
activeTabName === 'unifiedCanvas') && <ImageToImageStrength />}
|
||||||
|
{activeTabName === 'txt2img' && <HrfSettings />}
|
||||||
{activeTabName === 'unifiedCanvas' && (
|
{activeTabName === 'unifiedCanvas' && (
|
||||||
<>
|
<>
|
||||||
<ParamScaleBeforeProcessing />
|
<ParamScaleBeforeProcessing />
|
||||||
@ -100,23 +86,3 @@ export const ImageSettingsAccordion = memo(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ImageSettingsAccordion.displayName = 'ImageSettingsAccordion';
|
ImageSettingsAccordion.displayName = 'ImageSettingsAccordion';
|
||||||
|
|
||||||
const WidthHeight = memo((props: { activeTabName: InvokeTabName }) => {
|
|
||||||
if (props.activeTabName === 'unifiedCanvas') {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ParamBoundingBoxWidth />
|
|
||||||
<ParamBoundingBoxHeight />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ParamWidth />
|
|
||||||
<ParamHeight />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
WidthHeight.displayName = 'WidthHeight';
|
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
aspectRatioChanged,
|
||||||
|
setBoundingBoxDimensions,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import ParamBoundingBoxHeight from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight';
|
||||||
|
import ParamBoundingBoxWidth from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth';
|
||||||
|
import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize';
|
||||||
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const ImageSizeCanvas = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { width, height } = useAppSelector(
|
||||||
|
(state) => state.canvas.boundingBoxDimensions
|
||||||
|
);
|
||||||
|
const aspectRatioState = useAppSelector(
|
||||||
|
(state) => state.canvas.aspectRatio
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeWidth = useCallback(
|
||||||
|
(width: number) => {
|
||||||
|
dispatch(setBoundingBoxDimensions({ width }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeHeight = useCallback(
|
||||||
|
(height: number) => {
|
||||||
|
dispatch(setBoundingBoxDimensions({ height }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeAspectRatioState = useCallback(
|
||||||
|
(aspectRatioState: AspectRatioState) => {
|
||||||
|
dispatch(aspectRatioChanged(aspectRatioState));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageSize
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
aspectRatioState={aspectRatioState}
|
||||||
|
heightComponent={<ParamBoundingBoxHeight />}
|
||||||
|
widthComponent={<ParamBoundingBoxWidth />}
|
||||||
|
onChangeAspectRatioState={onChangeAspectRatioState}
|
||||||
|
onChangeWidth={onChangeWidth}
|
||||||
|
onChangeHeight={onChangeHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageSizeCanvas.displayName = 'ImageSizeCanvas';
|
@ -0,0 +1,56 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { ParamHeight } from 'features/parameters/components/Core/ParamHeight';
|
||||||
|
import { ParamWidth } from 'features/parameters/components/Core/ParamWidth';
|
||||||
|
import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize';
|
||||||
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
|
import {
|
||||||
|
aspectRatioChanged,
|
||||||
|
heightChanged,
|
||||||
|
widthChanged,
|
||||||
|
} from 'features/parameters/store/generationSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const ImageSizeLinear = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const width = useAppSelector((state) => state.generation.width);
|
||||||
|
const height = useAppSelector((state) => state.generation.height);
|
||||||
|
const aspectRatioState = useAppSelector(
|
||||||
|
(state) => state.generation.aspectRatio
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeWidth = useCallback(
|
||||||
|
(width: number) => {
|
||||||
|
dispatch(widthChanged(width));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeHeight = useCallback(
|
||||||
|
(height: number) => {
|
||||||
|
dispatch(heightChanged(height));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeAspectRatioState = useCallback(
|
||||||
|
(aspectRatioState: AspectRatioState) => {
|
||||||
|
dispatch(aspectRatioChanged(aspectRatioState));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageSize
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
aspectRatioState={aspectRatioState}
|
||||||
|
heightComponent={<ParamHeight />}
|
||||||
|
widthComponent={<ParamWidth />}
|
||||||
|
onChangeAspectRatioState={onChangeAspectRatioState}
|
||||||
|
onChangeWidth={onChangeWidth}
|
||||||
|
onChangeHeight={onChangeHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageSizeLinear.displayName = 'ImageSizeLinear';
|
Loading…
x
Reference in New Issue
Block a user