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:
psychedelicious 2024-01-02 12:01:23 +11:00 committed by Kent Keirsey
parent 011757c497
commit 4f43eda09b
27 changed files with 614 additions and 554 deletions

View File

@ -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',
};

View File

@ -1,7 +0,0 @@
import type { IconType } from 'react-icons';
export type AspectRatioPreviewProps = {
width: number;
height: number;
icon?: IconType;
};

View File

@ -10,8 +10,7 @@ import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants';
import floorCoordinates from 'features/canvas/util/floorCoordinates';
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
import roundDimensionsTo64 from 'features/canvas/util/roundDimensionsTo64';
import { ASPECT_RATIO_MAP } from 'features/parameters/components/ImageSize/constants';
import { aspectRatioSelected } from 'features/parameters/store/generationSlice';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
import type { RgbaColor } from 'react-colorful';
@ -83,6 +82,11 @@ export const initialCanvasState: CanvasState = {
stageScale: 1,
tool: 'brush',
batchIds: [],
aspectRatio: {
id: '1:1',
value: 1,
isLocked: false,
},
};
export const canvasSlice = createSlice({
@ -198,8 +202,14 @@ export const canvasSlice = createSlice({
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
},
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
const newDimensions = roundDimensionsTo64(action.payload);
setBoundingBoxDimensions: (
state,
action: PayloadAction<Partial<Dimensions>>
) => {
const newDimensions = roundDimensionsTo64({
...state.boundingBoxDimensions,
...action.payload,
});
state.boundingBoxDimensions = newDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
@ -722,6 +732,21 @@ export const canvasSlice = createSlice({
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) => {
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(
queueApi.endpoints.clearQueue.matchFulfilled,
(state) => {
@ -823,6 +833,7 @@ export const {
canvasResized,
canvasBatchIdAdded,
canvasBatchIdsReset,
aspectRatioChanged,
} = canvasSlice.actions;
export default canvasSlice.reducer;

View File

@ -1,3 +1,4 @@
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type { IRect, Vector2d } from 'konva/lib/types';
import type { RgbaColor } from 'react-colorful';
import { z } from 'zod';
@ -150,6 +151,7 @@ export interface CanvasState {
tool: CanvasTool;
generationMode?: GenerationMode;
batchIds: string[];
aspectRatio: AspectRatioState;
}
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';

View File

@ -1,77 +1,45 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
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 { InvSlider } from 'common/components/InvSlider/InvSlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
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 { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector(
[stateSelector, isStagingSelector],
({ canvas, generation }, isStaging) => {
const { boundingBoxDimensions } = canvas;
const { model, aspectRatio } = generation;
({ generation }, isStaging) => {
const { model } = generation;
const initial = ['sdxl', 'sdxl-refiner'].includes(
model?.base_model as string
)
? 1024
: 512;
return {
initial,
model,
boundingBoxDimensions,
isStaging,
aspectRatio,
};
}
);
const ParamBoundingBoxWidth = () => {
const dispatch = useAppDispatch();
const { model, boundingBoxDimensions, isStaging, aspectRatio } =
useAppSelector(selector);
const { isStaging, initial } = useAppSelector(selector);
const ctx = useImageSizeContext();
const { t } = useTranslation();
const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string)
? 1024
: 512;
const handleChangeHeight = useCallback(
const onChange = useCallback(
(v: number) => {
dispatch(
setBoundingBoxDimensions({
...boundingBoxDimensions,
height: Math.floor(v),
})
);
if (aspectRatio) {
const newWidth = roundToMultiple(v * aspectRatio.value, 64);
dispatch(
setBoundingBoxDimensions({
width: newWidth,
height: Math.floor(v),
})
);
}
ctx.heightChanged(v);
},
[aspectRatio, boundingBoxDimensions, dispatch]
[ctx]
);
const handleResetHeight = useCallback(() => {
dispatch(
setBoundingBoxDimensions({
...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]);
const onReset = useCallback(() => {
ctx.heightChanged(initial);
}, [ctx, initial]);
return (
<InvControl label={t('parameters.height')} isDisabled={isStaging}>
@ -79,9 +47,9 @@ const ParamBoundingBoxWidth = () => {
min={64}
max={1536}
step={64}
value={boundingBoxDimensions.height}
onChange={handleChangeHeight}
onReset={handleResetHeight}
value={ctx.height}
onChange={onChange}
onReset={onReset}
marks
withNumberInput
numberInputMax={4096}

View File

@ -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 {};

View File

@ -1,77 +1,45 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
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 { InvSlider } from 'common/components/InvSlider/InvSlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
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 { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector(
[stateSelector, isStagingSelector],
({ canvas, generation }, isStaging) => {
const { boundingBoxDimensions } = canvas;
const { model, aspectRatio } = generation;
({ generation }, isStaging) => {
const { model } = generation;
const initial = ['sdxl', 'sdxl-refiner'].includes(
model?.base_model as string
)
? 1024
: 512;
return {
initial,
model,
boundingBoxDimensions,
isStaging,
aspectRatio,
};
}
);
const ParamBoundingBoxWidth = () => {
const dispatch = useAppDispatch();
const { model, boundingBoxDimensions, isStaging, aspectRatio } =
useAppSelector(selector);
const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string)
? 1024
: 512;
const { isStaging, initial } = useAppSelector(selector);
const ctx = useImageSizeContext();
const { t } = useTranslation();
const handleChangeWidth = useCallback(
const onChange = useCallback(
(v: number) => {
dispatch(
setBoundingBoxDimensions({
...boundingBoxDimensions,
width: Math.floor(v),
})
);
if (aspectRatio) {
const newHeight = roundToMultiple(v / aspectRatio.value, 64);
dispatch(
setBoundingBoxDimensions({
width: Math.floor(v),
height: newHeight,
})
);
}
ctx.widthChanged(v);
},
[aspectRatio, boundingBoxDimensions, dispatch]
[ctx]
);
const handleResetWidth = useCallback(() => {
dispatch(
setBoundingBoxDimensions({
...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]);
const onReset = useCallback(() => {
ctx.widthChanged(initial);
}, [ctx, initial]);
return (
<InvControl label={t('parameters.width')} isDisabled={isStaging}>
@ -79,9 +47,9 @@ const ParamBoundingBoxWidth = () => {
min={64}
max={1536}
step={64}
value={boundingBoxDimensions.width}
onChange={handleChangeWidth}
onReset={handleResetWidth}
value={ctx.width}
onChange={onChange}
onReset={onReset}
withNumberInput
numberInputMax={4096}
marks

View File

@ -1,10 +1,10 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
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 { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
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 { useTranslation } from 'react-i18next';
@ -12,7 +12,7 @@ const selector = createMemoizedSelector(
[stateSelector],
({ generation, config }) => {
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height;
const { model, height } = generation;
const { model } = generation;
const initial = ['sdxl', 'sdxl-refiner'].includes(
model?.base_model as string
@ -22,7 +22,6 @@ const selector = createMemoizedSelector(
return {
initial,
height,
min,
max: sliderMax,
inputMax,
@ -34,27 +33,27 @@ const selector = createMemoizedSelector(
export const ParamHeight = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { initial, height, min, max, inputMax, step, fineStep } =
const ctx = useImageSizeContext();
const { initial, min, max, inputMax, step, fineStep } =
useAppSelector(selector);
const onChange = useCallback(
(v: number) => {
dispatch(heightChanged(v));
ctx.heightChanged(v);
},
[dispatch]
[ctx]
);
const onReset = useCallback(() => {
dispatch(heightChanged(initial));
}, [dispatch, initial]);
ctx.heightChanged(initial);
}, [ctx, initial]);
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
return (
<InvControl label={t('parameters.height')}>
<InvSlider
value={height}
value={ctx.height}
onChange={onChange}
onReset={onReset}
min={min}
@ -64,7 +63,7 @@ export const ParamHeight = memo(() => {
marks={marks}
/>
<InvNumberInput
value={height}
value={ctx.height}
onChange={onChange}
min={min}
max={inputMax}

View File

@ -1,10 +1,10 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
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 { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
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 { useTranslation } from 'react-i18next';
@ -12,7 +12,7 @@ const selector = createMemoizedSelector(
[stateSelector],
({ generation, config }) => {
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width;
const { model, width } = generation;
const { model } = generation;
const initial = ['sdxl', 'sdxl-refiner'].includes(
model?.base_model as string
@ -22,7 +22,6 @@ const selector = createMemoizedSelector(
return {
initial,
width,
min,
max: sliderMax,
step: coarseStep,
@ -33,27 +32,27 @@ const selector = createMemoizedSelector(
);
export const ParamWidth = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { initial, width, min, max, inputMax, step, fineStep } =
const ctx = useImageSizeContext();
const { initial, min, max, inputMax, step, fineStep } =
useAppSelector(selector);
const onChange = useCallback(
(v: number) => {
dispatch(widthChanged(v));
ctx.widthChanged(v);
},
[dispatch]
[ctx]
);
const onReset = useCallback(() => {
dispatch(widthChanged(initial));
}, [dispatch, initial]);
ctx.widthChanged(initial);
}, [ctx, initial]);
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
return (
<InvControl label={t('parameters.width')}>
<InvSlider
value={width}
value={ctx.width}
onChange={onChange}
onReset={onReset}
min={min}
@ -63,7 +62,7 @@ export const ParamWidth = memo(() => {
marks={marks}
/>
<InvNumberInput
value={width}
value={ctx.width}
onChange={onChange}
min={min}
max={inputMax}

View File

@ -1,29 +1,55 @@
import { Flex, Icon } from '@chakra-ui/react';
import { useSize } from '@chakra-ui/react-use-size';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
import { useMemo, useRef } from 'react';
import { FaImage } from 'react-icons/fa';
import {
BOX_SIZE_CSS_CALC,
ICON_CONTAINER_STYLES,
ICON_HIGH_CUTOFF,
ICON_LOW_CUTOFF,
MOTION_ICON_ANIMATE,
MOTION_ICON_EXIT,
MOTION_ICON_INITIAL,
} from './constants';
import { useAspectRatioPreviewState } from './hooks';
import type { AspectRatioPreviewProps } from './types';
export const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
const { width: _width, height: _height, icon = FaImage } = props;
export type AspectRatioPreviewProps = {
width: number;
height: number;
};
export const AspectRatioPreview = () => {
const ctx = useImageSizeContext();
const containerRef = useRef<HTMLDivElement>(null);
const containerSize = useSize(containerRef);
const { width, height, shouldShowIcon } = useAspectRatioPreviewState({
width: _width,
height: _height,
containerSize,
});
const shouldShowIcon = useMemo(
() =>
ctx.aspectRatioState.value < ICON_HIGH_CUTOFF &&
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 (
<Flex
@ -50,7 +76,7 @@ export const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
exit={MOTION_ICON_EXIT}
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>
)}
</AnimatePresence>

View File

@ -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';

View File

@ -1,39 +1,34 @@
import type { SystemStyleObject } from '@chakra-ui/styled-system';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { SingleValue } from 'chakra-react-select';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import type { InvSelectOption } from 'common/components/InvSelect/types';
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 { aspectRatioSelected } from 'features/parameters/store/generationSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { LockAspectRatioButton } from './LockAspectRatioButton';
import { SetOptimalSizeButton } from './SetOptimalSizeButton';
import { SwapDimensionsButton } from './SwapDimensionsButton';
export const AspectRatioSelect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const aspectRatioID = useAppSelector(
(state) => state.generation.aspectRatio.id
);
const ctx = useImageSizeContext();
const onChange = useCallback(
(v: SingleValue<InvSelectOption>) => {
if (!v || !isAspectRatioID(v.value)) {
return;
}
dispatch(aspectRatioSelected(v.value));
ctx.aspectRatioSelected(v.value);
},
[dispatch]
[ctx]
);
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 (
@ -44,9 +39,6 @@ export const AspectRatioSelect = memo(() => {
options={ASPECT_RATIO_OPTIONS}
sx={selectStyles}
/>
<SwapDimensionsButton />
<LockAspectRatioButton />
<SetOptimalSizeButton />
</InvControl>
);
});

View File

@ -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,
};

View File

@ -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;
};

View File

@ -1,27 +1,23 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
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 { useTranslation } from 'react-i18next';
import { FaLock, FaLockOpen } from 'react-icons/fa6';
export const LockAspectRatioButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isLocked = useAppSelector(
(state) => state.generation.aspectRatio.isLocked
);
const ctx = useImageSizeContext();
const onClick = useCallback(() => {
dispatch(isLockedToggled());
}, [dispatch]);
ctx.isLockedToggled();
}, [ctx]);
return (
<InvIconButton
aria-label={t('parameters.lockAspectRatio')}
onClick={onClick}
variant={isLocked ? 'outline' : 'ghost'}
variant={ctx.aspectRatioState.isLocked ? 'outline' : 'ghost'}
size="sm"
icon={isLocked ? <FaLock /> : <FaLockOpen />}
icon={ctx.aspectRatioState.isLocked ? <FaLock /> : <FaLockOpen />}
/>
);
});

View File

@ -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 { sizeReset } from 'features/parameters/store/generationSlice';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { IoSparkles } from 'react-icons/io5';
export const SetOptimalSizeButton = memo(() => {
const { t } = useTranslation();
const ctx = useImageSizeContext();
const optimalDimension = useAppSelector((state) =>
state.generation.model?.base_model === 'sdxl' ? 1024 : 512
);
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(sizeReset(optimalDimension));
}, [dispatch, optimalDimension]);
ctx.sizeReset(optimalDimension, optimalDimension);
}, [ctx, optimalDimension]);
return (
<InvIconButton

View File

@ -1,16 +1,15 @@
import { useAppDispatch } from 'app/store/storeHooks';
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 { useTranslation } from 'react-i18next';
import { IoSwapVertical } from 'react-icons/io5';
export const SwapDimensionsButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const ctx = useImageSizeContext();
const onClick = useCallback(() => {
dispatch(dimensionsSwapped());
}, [dispatch]);
ctx.dimensionsSwapped();
}, [ctx]);
return (
<InvIconButton
aria-label={t('parameters.swapDimensions')}

View File

@ -1,6 +1,30 @@
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[] = [
{ label: 'Free' as const, value: 'Free' },
@ -25,3 +49,9 @@ export const ASPECT_RATIO_MAP: Record<
'2:3': { ratio: 2 / 3, inverseID: '3:2' },
'9:16': { ratio: 9 / 16, inverseID: '16:9' },
};
export const initialAspectRatioState: AspectRatioState = {
id: '1:1',
value: 1,
isLocked: false,
};

View File

@ -13,3 +13,9 @@ export const zAspectRatioID = z.enum([
export type AspectRatioID = z.infer<typeof zAspectRatioID>;
export const isAspectRatioID = (v: string): v is AspectRatioID =>
zAspectRatioID.safeParse(v).success;
export type AspectRatioState = {
id: AspectRatioID;
value: number;
isLocked: boolean;
};

View File

@ -1,4 +1,4 @@
import type { GenerationState } from './generationSlice';
import type { GenerationState } from './types';
/**
* Generation slice persist denylist

View File

@ -2,77 +2,25 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { ASPECT_RATIO_MAP } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioID } from 'features/parameters/components/ImageSize/types';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
import type {
ParameterCanvasCoherenceMode,
ParameterCFGRescaleMultiplier,
ParameterCFGScale,
ParameterHeight,
ParameterMaskBlurMethod,
ParameterModel,
ParameterNegativePrompt,
ParameterPositivePrompt,
ParameterPrecision,
ParameterScheduler,
ParameterSeed,
ParameterSteps,
ParameterStrength,
ParameterVAEModel,
ParameterWidth,
} from 'features/parameters/types/parameterSchemas';
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
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';
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: {
id: AspectRatioID;
value: number;
isLocked: boolean;
};
}
import type { GenerationState } from './types';
export const initialGenerationState: GenerationState = {
cfgScale: 7.5,
@ -112,18 +60,12 @@ export const initialGenerationState: GenerationState = {
clipSkip: 0,
shouldUseCpuNoise: true,
shouldShowAdvancedOptions: false,
aspectRatio: {
id: '1:1',
value: 1,
isLocked: false,
},
aspectRatio: { ...initialAspectRatioState },
};
const initialState: GenerationState = initialGenerationState;
export const generationSlice = createSlice({
name: 'generation',
initialState,
initialState: initialGenerationState,
reducers: {
setPositivePrompt: (state, action: PayloadAction<string>) => {
state.positivePrompt = action.payload;
@ -279,93 +221,15 @@ export const generationSlice = createSlice({
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
state.shouldUseCpuNoise = action.payload;
},
aspectRatioSelected: (
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);
widthChanged: (state, action: PayloadAction<number>) => {
state.width = action.payload;
},
heightChanged: (state, action: PayloadAction<number>) => {
state.height = action.payload;
},
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
state.aspectRatio = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(configChanged, (state, action) => {
@ -436,13 +300,10 @@ export const {
setSeamlessYAxis,
setClipSkip,
shouldUseCpuNoiseChanged,
aspectRatioSelected,
dimensionsSwapped,
vaePrecisionChanged,
aspectRatioChanged,
widthChanged,
heightChanged,
isLockedToggled,
sizeReset,
vaePrecisionChanged,
} = generationSlice.actions;
export default generationSlice.reducer;

View 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;
}

View File

@ -7,25 +7,21 @@ import type { InvLabelProps } from 'common/components/InvControl/types';
import { InvExpander } from 'common/components/InvExpander/InvExpander';
import { InvSingleAccordion } from 'common/components/InvSingleAccordion/InvSingleAccordion';
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 ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
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 ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { ImageSizeCanvas } from './ImageSizeCanvas';
import { ImageSizeLinear } from './ImageSizeLinear';
const selector = createMemoizedSelector(
[stateSelector, activeTabNameSelector],
({ generation, hrf }, activeTabName) => {
@ -42,10 +38,6 @@ const selector = createMemoizedSelector(
}
);
const labelProps: InvLabelProps = {
minW: 12,
};
const scalingLabelProps: InvLabelProps = {
minW: '4.5rem',
};
@ -61,17 +53,11 @@ export const ImageSettingsAccordion = memo(() => {
badges={badges}
>
<Flex px={4} pt={4} w="full" h="full" flexDir="column">
<Flex gap={4} alignItems="center">
<Flex gap={4} flexDirection="column" width="full">
<InvControlGroup labelProps={labelProps}>
<AspectRatioSelect />
<WidthHeight activeTabName={activeTabName} />
</InvControlGroup>
</Flex>
<Flex w="98px" h="98px" flexShrink={0} flexGrow={0}>
<AspectRatioPreviewWrapper />
</Flex>
</Flex>
{activeTabName === 'unifiedCanvas' ? (
<ImageSizeCanvas />
) : (
<ImageSizeLinear />
)}
<InvExpander>
<Flex gap={4} pb={4} flexDir="column">
<Flex gap={4}>
@ -79,10 +65,10 @@ export const ImageSettingsAccordion = memo(() => {
<ParamSeedShuffle />
<ParamSeedRandomize />
</Flex>
{activeTabName === 'txt2img' && <HrfSettings />}
{activeTabName === 'img2img' && <ImageToImageFit />}
{(activeTabName === 'img2img' ||
activeTabName === 'unifiedCanvas') && <ImageToImageStrength />}
{activeTabName === 'txt2img' && <HrfSettings />}
{activeTabName === 'unifiedCanvas' && (
<>
<ParamScaleBeforeProcessing />
@ -100,23 +86,3 @@ export const ImageSettingsAccordion = memo(() => {
});
ImageSettingsAccordion.displayName = 'ImageSettingsAccordion';
const WidthHeight = memo((props: { activeTabName: InvokeTabName }) => {
if (props.activeTabName === 'unifiedCanvas') {
return (
<>
<ParamBoundingBoxWidth />
<ParamBoundingBoxHeight />
</>
);
}
return (
<>
<ParamWidth />
<ParamHeight />
</>
);
});
WidthHeight.displayName = 'WidthHeight';

View File

@ -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';

View File

@ -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';