diff --git a/invokeai/frontend/web/src/common/components/AspectRatioPreview/constants.ts b/invokeai/frontend/web/src/common/components/AspectRatioPreview/constants.ts deleted file mode 100644 index 34f3a6941a..0000000000 --- a/invokeai/frontend/web/src/common/components/AspectRatioPreview/constants.ts +++ /dev/null @@ -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', -}; diff --git a/invokeai/frontend/web/src/common/components/AspectRatioPreview/types.ts b/invokeai/frontend/web/src/common/components/AspectRatioPreview/types.ts deleted file mode 100644 index 7319e7112b..0000000000 --- a/invokeai/frontend/web/src/common/components/AspectRatioPreview/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { IconType } from 'react-icons'; - -export type AspectRatioPreviewProps = { - width: number; - height: number; - icon?: IconType; -}; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index f1e5a88242..9775a4ab25 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -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) => { - const newDimensions = roundDimensionsTo64(action.payload); + setBoundingBoxDimensions: ( + state, + action: PayloadAction> + ) => { + 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) => { + 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; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index ae778ae11e..7eafad5d46 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -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'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx index 731d8857ef..0cf3f3129c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx @@ -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 ( @@ -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} diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxSize.tsx deleted file mode 100644 index 5c9a24b2de..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxSize.tsx +++ /dev/null @@ -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 ( -// -// -// -// {t('parameters.aspectRatio')} -// -// -// } -// fontSize={20} -// onClick={handleToggleSize} -// /> -// } -// isChecked={shouldLockAspectRatio} -// onClick={handleLockRatio} -// /> -// -// -// -// -// -// ); -// } -export default {}; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx index 34c9bc414e..aef6c6f32e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx @@ -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 ( @@ -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 diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index 5b2e1ff771..fc905044af 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -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 ( { marks={marks} /> { 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 ( { marks={marks} /> { - const { width: _width, height: _height, icon = FaImage } = props; +export type AspectRatioPreviewProps = { + width: number; + height: number; +}; + +export const AspectRatioPreview = () => { + const ctx = useImageSizeContext(); const containerRef = useRef(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 ( { exit={MOTION_ICON_EXIT} style={ICON_CONTAINER_STYLES} > - + )} diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreviewWrapper.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreviewWrapper.tsx deleted file mode 100644 index 846e802cb1..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreviewWrapper.tsx +++ /dev/null @@ -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 ; -}); - -AspectRatioPreviewWrapper.displayName = 'AspectRatioPreviewWrapper'; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx index 75129d5d33..5e90a12d8c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx @@ -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) => { 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} /> - - - ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSize.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSize.tsx new file mode 100644 index 0000000000..9120ec4fa2 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSize.tsx @@ -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 ( + + + + + + + + + + + {widthComponent} + {heightComponent} + + + + + + + + ); +}); + +ImageSize.displayName = 'ImageSize'; + +const labelProps: InvLabelProps = { + minW: 14, +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts new file mode 100644 index 0000000000..e9d573387d --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts @@ -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(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; +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx index 39c8ea46d1..d5ab2ac2a0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx @@ -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 ( : } + icon={ctx.aspectRatioState.isLocked ? : } /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx index dc3defcc9f..a9beb030be 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx @@ -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 ( { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const ctx = useImageSizeContext(); const onClick = useCallback(() => { - dispatch(dimensionsSwapped()); - }, [dispatch]); + ctx.dimensionsSwapped(); + }, [ctx]); return ( ; export const isAspectRatioID = (v: string): v is AspectRatioID => zAspectRatioID.safeParse(v).success; + +export type AspectRatioState = { + id: AspectRatioID; + value: number; + isLocked: boolean; +}; diff --git a/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts b/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts index 03d518f784..2003566bbd 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts @@ -1,4 +1,4 @@ -import type { GenerationState } from './generationSlice'; +import type { GenerationState } from './types'; /** * Generation slice persist denylist diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 85fe4d9125..8a5526b3bd 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -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) => { state.positivePrompt = action.payload; @@ -279,93 +221,15 @@ export const generationSlice = createSlice({ shouldUseCpuNoiseChanged: (state, action: PayloadAction) => { state.shouldUseCpuNoise = action.payload; }, - aspectRatioSelected: ( - state, - action: PayloadAction - ) => { - 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) => { - 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 - ) => { - 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) => { - state.aspectRatio = cloneDeep(initialGenerationState.aspectRatio); + widthChanged: (state, action: PayloadAction) => { state.width = action.payload; + }, + heightChanged: (state, action: PayloadAction) => { state.height = action.payload; }, + aspectRatioChanged: (state, action: PayloadAction) => { + 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; diff --git a/invokeai/frontend/web/src/features/parameters/store/types.ts b/invokeai/frontend/web/src/features/parameters/store/types.ts new file mode 100644 index 0000000000..e78acfbfd3 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/types.ts @@ -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; +} diff --git a/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 37ede9f600..340d4beb6c 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -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} > - - - - - - - - - - - + {activeTabName === 'unifiedCanvas' ? ( + + ) : ( + + )} @@ -79,10 +65,10 @@ export const ImageSettingsAccordion = memo(() => { - {activeTabName === 'txt2img' && } {activeTabName === 'img2img' && } {(activeTabName === 'img2img' || activeTabName === 'unifiedCanvas') && } + {activeTabName === 'txt2img' && } {activeTabName === 'unifiedCanvas' && ( <> @@ -100,23 +86,3 @@ export const ImageSettingsAccordion = memo(() => { }); ImageSettingsAccordion.displayName = 'ImageSettingsAccordion'; - -const WidthHeight = memo((props: { activeTabName: InvokeTabName }) => { - if (props.activeTabName === 'unifiedCanvas') { - return ( - <> - - - - ); - } - - return ( - <> - - - - ); -}); - -WidthHeight.displayName = 'WidthHeight'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeCanvas.tsx b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeCanvas.tsx new file mode 100644 index 0000000000..811ccd464d --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeCanvas.tsx @@ -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 ( + } + widthComponent={} + onChangeAspectRatioState={onChangeAspectRatioState} + onChangeWidth={onChangeWidth} + onChangeHeight={onChangeHeight} + /> + ); +}); + +ImageSizeCanvas.displayName = 'ImageSizeCanvas'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeLinear.tsx new file mode 100644 index 0000000000..c32092a493 --- /dev/null +++ b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -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 ( + } + widthComponent={} + onChangeAspectRatioState={onChangeAspectRatioState} + onChangeWidth={onChangeWidth} + onChangeHeight={onChangeHeight} + /> + ); +}); + +ImageSizeLinear.displayName = 'ImageSizeLinear';