diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index a94b0b45e3..fa1e453ff9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -4,16 +4,16 @@ import type { AppDispatch, RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; import { caModelChanged, - heightChanged, + documentHeightChanged, + documentWidthChanged, ipaModelChanged, loraDeleted, modelChanged, refinerModelChanged, rgIPAdapterModelChanged, vaeSelected, - widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; @@ -91,8 +91,8 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { optimalDimension * optimalDimension ); - dispatch(widthChanged({ width })); - dispatch(heightChanged({ height })); + dispatch(documentWidthChanged({ width })); + dispatch(documentHeightChanged({ height })); return; } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index fa01ee9e65..f8ddadb488 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,13 +1,13 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - heightChanged, + documentHeightChanged, + documentWidthChanged, setCfgRescaleMultiplier, setCfgScale, setScheduler, setSteps, vaePrecisionChanged, vaeSelected, - widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; import { setDefaultSettings } from 'features/parameters/store/actions'; import { @@ -99,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni const setSizeOptions = { updateAspectRatio: true, clamp: true }; if (width) { if (isParameterWidth(width)) { - dispatch(widthChanged({ width, ...setSizeOptions })); + dispatch(documentWidthChanged({ width, ...setSizeOptions })); } } if (height) { if (isParameterHeight(height)) { - dispatch(heightChanged({ height, ...setSizeOptions })); + dispatch(documentHeightChanged({ height, ...setSizeOptions })); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index b87362e573..5d6d253fbf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -3,11 +3,11 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { documentHeightChanged, documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; @@ -89,15 +89,15 @@ export const CAImagePreview = memo( if (shift) { const { width, height } = controlImage; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } }, [controlImage, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx index 9de19ea3ef..1454e102b0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -3,11 +3,11 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { documentHeightChanged, documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { memo, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; @@ -42,15 +42,15 @@ export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppa const options = { updateAspectRatio: true, clamp: true }; if (shift) { const { width, height } = controlImage; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } }, [controlImage, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index ff345dfe13..5f07dde123 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -2,10 +2,10 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; -import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; +import { documentReducers } from 'features/controlLayers/store/documentReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; @@ -15,8 +15,7 @@ import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; import { toolReducers } from 'features/controlLayers/store/toolReducers'; -import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; +import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; @@ -145,27 +144,7 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, ...inpaintMaskReducers, ...sessionReducers, - widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { - const { width, updateAspectRatio, clamp } = action.payload; - state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; - if (updateAspectRatio) { - state.document.aspectRatio.value = state.document.width / state.document.height; - state.document.aspectRatio.id = 'Free'; - state.document.aspectRatio.isLocked = false; - } - }, - heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { - const { height, updateAspectRatio, clamp } = action.payload; - state.document.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; - if (updateAspectRatio) { - state.document.aspectRatio.value = state.document.width / state.document.height; - state.document.aspectRatio.id = 'Free'; - state.document.aspectRatio.isLocked = false; - } - }, - aspectRatioChanged: (state, action: PayloadAction) => { - state.document.aspectRatio = action.payload; - }, + ...documentReducers, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, @@ -192,9 +171,6 @@ export const canvasV2Slice = createSlice({ }); export const { - widthChanged, - heightChanged, - aspectRatioChanged, bboxChanged, brushWidthChanged, eraserWidthChanged, @@ -209,6 +185,13 @@ export const { bboxScaleMethodChanged, clipToBboxChanged, canvasReset, + // document + documentWidthChanged, + documentHeightChanged, + documentAspectRatioLockToggled, + documentAspectRatioIdChanged, + documentDimensionsSwapped, + documentSizeOptimized, // layers layerAdded, layerRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts new file mode 100644 index 0000000000..2d14f05cd1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts @@ -0,0 +1,104 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; +import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; +import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types'; +import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; + +export const documentReducers = { + documentWidthChanged: ( + state, + action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> + ) => { + const { width, updateAspectRatio, clamp } = action.payload; + state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; + + if (state.document.aspectRatio.isLocked) { + state.document.height = roundToMultiple(state.document.width / state.document.aspectRatio.value, 8); + } + + if (updateAspectRatio || !state.document.aspectRatio.isLocked) { + state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.id = 'Free'; + state.document.aspectRatio.isLocked = false; + } + + if (!state.session.isActive) { + state.bbox.width = state.document.width; + state.bbox.height = state.document.height; + } + }, + documentHeightChanged: ( + state, + action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> + ) => { + const { height, updateAspectRatio, clamp } = action.payload; + + state.document.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; + + if (state.document.aspectRatio.isLocked) { + state.document.width = roundToMultiple(state.document.height * state.document.aspectRatio.value, 8); + } + + if (updateAspectRatio || !state.document.aspectRatio.isLocked) { + state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.id = 'Free'; + state.document.aspectRatio.isLocked = false; + } + + if (!state.session.isActive) { + state.bbox.width = state.document.width; + state.bbox.height = state.document.height; + } + }, + documentAspectRatioLockToggled: (state) => { + state.document.aspectRatio.isLocked = !state.document.aspectRatio.isLocked; + }, + documentAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { + const { id } = action.payload; + state.document.aspectRatio.id = id; + if (id === 'Free') { + state.document.aspectRatio.isLocked = false; + } else { + state.document.aspectRatio.isLocked = true; + state.document.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + const { width, height } = calculateNewSize( + state.document.aspectRatio.value, + state.document.width * state.document.height + ); + state.document.width = width; + state.document.height = height; + } + }, + documentDimensionsSwapped: (state) => { + state.document.aspectRatio.value = 1 / state.document.aspectRatio.value; + if (state.document.aspectRatio.id === 'Free') { + const newWidth = state.document.height; + const newHeight = state.document.width; + state.document.width = newWidth; + state.document.height = newHeight; + } else { + const { width, height } = calculateNewSize( + state.document.aspectRatio.value, + state.document.width * state.document.height + ); + state.document.width = width; + state.document.height = height; + state.document.aspectRatio.id = ASPECT_RATIO_MAP[state.document.aspectRatio.id].inverseID; + } + }, + documentSizeOptimized: (state) => { + const optimalDimension = getOptimalDimension(state.params.model); + if (state.document.aspectRatio.isLocked) { + const { width, height } = calculateNewSize(state.document.aspectRatio.value, optimalDimension ** 2); + state.document.width = width; + state.document.height = height; + } else { + state.document.aspectRatio = deepClone(initialAspectRatioState); + state.document.width = optimalDimension; + state.document.height = optimalDimension; + } + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index fe7f895651..ac7abd6615 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -1,7 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import type { ParameterCFGRescaleMultiplier, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f31ce9e998..23688e7091 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -4,7 +4,7 @@ import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; import { getImageObjectId } from 'features/controlLayers/konva/naming'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; +import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; import type { ParameterCanvasCoherenceMode, ParameterCFGRescaleMultiplier, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index e2f1c21e8a..de9efe32ce 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -12,7 +12,8 @@ import { } from 'features/controlLayers/konva/naming'; import { caRecalled, - heightChanged, + documentHeightChanged, + documentWidthChanged, ipaRecalled, layerAllDeleted, layerRecalled, @@ -37,7 +38,6 @@ import { setSeed, setSteps, vaeSelected, - widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { ControlAdapterEntity, @@ -115,11 +115,11 @@ const recallScheduler: MetadataRecallFunc = (scheduler) => { const setSizeOptions = { updateAspectRatio: true, clamp: true }; const recallWidth: MetadataRecallFunc = (width) => { - getStore().dispatch(widthChanged({ width, ...setSizeOptions })); + getStore().dispatch(documentWidthChanged({ width, ...setSizeOptions })); }; const recallHeight: MetadataRecallFunc = (height) => { - getStore().dispatch(heightChanged({ height, ...setSizeOptions })); + getStore().dispatch(documentHeightChanged({ height, ...setSizeOptions })); }; const recallSteps: MetadataRecallFunc = (steps) => { 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 63fc4ed632..68a2c05c16 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -1,15 +1,16 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { documentHeightChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamHeight = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); + const height = useAppSelector((s) => s.canvasV2.document.height); const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.height.numberInputMin); @@ -19,9 +20,9 @@ export const ParamHeight = memo(() => { const onChange = useCallback( (v: number) => { - ctx.heightChanged(v); + dispatch(documentHeightChanged({ height: v })); }, - [ctx] + [dispatch] ); const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); @@ -32,7 +33,7 @@ export const ParamHeight = memo(() => { {t('parameters.height')} { marks={marks} /> { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const width = useAppSelector((s) => s.canvasV2.document.width); const optimalDimension = useAppSelector(selectOptimalDimension); const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax); @@ -19,9 +20,9 @@ export const ParamWidth = memo(() => { const onChange = useCallback( (v: number) => { - ctx.widthChanged(v); + dispatch(documentWidthChanged({ width: v })); }, - [ctx] + [dispatch] ); const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); @@ -32,7 +33,7 @@ export const ParamWidth = memo(() => { {t('parameters.width')} { marks={marks} /> { diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioIconPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx similarity index 75% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioIconPreview.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx index 3ed7d0d802..b69cd0666d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioIconPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx @@ -1,6 +1,6 @@ import { useSize } from '@chakra-ui/react-use-size'; import { Flex, Icon } from '@invoke-ai/ui-library'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { useAppSelector } from 'app/store/storeHooks'; import { AnimatePresence, motion } from 'framer-motion'; import { memo, useMemo, useRef } from 'react'; import { PiFrameCorners } from 'react-icons/pi'; @@ -16,13 +16,13 @@ import { } from './constants'; export const AspectRatioIconPreview = memo(() => { - const ctx = useImageSizeContext(); + const document = useAppSelector((s) => s.canvasV2.document); const containerRef = useRef(null); const containerSize = useSize(containerRef); const shouldShowIcon = useMemo( - () => ctx.aspectRatioState.value < ICON_HIGH_CUTOFF && ctx.aspectRatioState.value > ICON_LOW_CUTOFF, - [ctx.aspectRatioState.value] + () => document.aspectRatio.value < ICON_HIGH_CUTOFF && document.aspectRatio.value > ICON_LOW_CUTOFF, + [document.aspectRatio.value] ); const { width, height } = useMemo(() => { @@ -30,19 +30,19 @@ export const AspectRatioIconPreview = memo(() => { return { width: 0, height: 0 }; } - let width = ctx.width; - let height = ctx.height; + let width = document.width; + let height = document.height; - if (ctx.width > ctx.height) { + if (document.width > document.height) { width = containerSize.width; - height = width / ctx.aspectRatioState.value; + height = width / document.aspectRatio.value; } else { height = containerSize.height; - width = height * ctx.aspectRatioState.value; + width = height * document.aspectRatio.value; } return { width, height }; - }, [containerSize, ctx.width, ctx.height, ctx.aspectRatioState.value]); + }, [containerSize, document.width, document.height, document.aspectRatio.value]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx similarity index 70% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx index 211cdba500..92256b3b3f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx @@ -1,31 +1,30 @@ import type { ComboboxOption, SystemStyleObject } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { SingleValue } from 'chakra-react-select'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -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 { documentAspectRatioIdChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/DocumentSize/constants'; +import { isAspectRatioID } from 'features/parameters/components/DocumentSize/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const AspectRatioSelect = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const id = useAppSelector((s) => s.canvasV2.document.aspectRatio.id); const onChange = useCallback( (v: SingleValue) => { if (!v || !isAspectRatioID(v.value)) { return; } - ctx.aspectRatioSelected(v.value); + dispatch(documentAspectRatioIdChanged({ id: v.value })); }, - [ctx] + [dispatch] ); - const value = useMemo( - () => ASPECT_RATIO_OPTIONS.filter((o) => o.value === ctx.aspectRatioState.id)[0], - [ctx.aspectRatioState.id] - ); + const value = useMemo(() => ASPECT_RATIO_OPTIONS.filter((o) => o.value === id)[0], [id]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx new file mode 100644 index 0000000000..97992f093c --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx @@ -0,0 +1,38 @@ +import type { FormLabelProps } from '@invoke-ai/ui-library'; +import { Flex, FormControlGroup } from '@invoke-ai/ui-library'; +import { ParamHeight } from 'features/parameters/components/Core/ParamHeight'; +import { ParamWidth } from 'features/parameters/components/Core/ParamWidth'; +import { AspectRatioIconPreview } from 'features/parameters/components/DocumentSize/AspectRatioIconPreview'; +import { AspectRatioSelect } from 'features/parameters/components/DocumentSize/AspectRatioSelect'; +import { LockAspectRatioButton } from 'features/parameters/components/DocumentSize/LockAspectRatioButton'; +import { SetOptimalSizeButton } from 'features/parameters/components/DocumentSize/SetOptimalSizeButton'; +import { SwapDimensionsButton } from 'features/parameters/components/DocumentSize/SwapDimensionsButton'; +import { memo } from 'react'; + +export const DocumentSize = memo(() => { + return ( + + + + + + + + + + + + + + + + + + ); +}); + +DocumentSize.displayName = 'DocumentSize'; + +const formLabelProps: FormLabelProps = { + minW: 14, +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx similarity index 55% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx index f24a52b12e..1d239d201f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx @@ -1,24 +1,26 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { documentAspectRatioLockToggled } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; export const LockAspectRatioButton = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const isLocked = useAppSelector((s) => s.canvasV2.document.aspectRatio.isLocked); const onClick = useCallback(() => { - ctx.isLockedToggled(); - }, [ctx]); + dispatch(documentAspectRatioLockToggled()); + }, [dispatch]); return ( : } + icon={isLocked ? : } /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx similarity index 68% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx index 4ebdf583d0..7becf2e36f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { documentSizeOptimized } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,19 +9,21 @@ import { RiSparklingFill } from 'react-icons/ri'; export const SetOptimalSizeButton = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const width = useAppSelector((s) => s.canvasV2.document.width); + const height = useAppSelector((s) => s.canvasV2.document.height); const optimalDimension = useAppSelector(selectOptimalDimension); const isSizeTooSmall = useMemo( - () => getIsSizeTooSmall(ctx.width, ctx.height, optimalDimension), - [ctx.height, ctx.width, optimalDimension] + () => getIsSizeTooSmall(width, height, optimalDimension), + [height, width, optimalDimension] ); const isSizeTooLarge = useMemo( - () => getIsSizeTooLarge(ctx.width, ctx.height, optimalDimension), - [ctx.height, ctx.width, optimalDimension] + () => getIsSizeTooLarge(width, height, optimalDimension), + [height, width, optimalDimension] ); const onClick = useCallback(() => { - ctx.setOptimalSize(); - }, [ctx]); + dispatch(documentSizeOptimized()); + }, [dispatch]); const tooltip = useMemo(() => { if (isSizeTooSmall) { return t('parameters.setToOptimalSizeTooSmall'); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx similarity index 71% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx index 80cfc6952c..bc57ca3f51 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx @@ -1,15 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { documentDimensionsSwapped } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; export const SwapDimensionsButton = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); const onClick = useCallback(() => { - ctx.dimensionsSwapped(); - }, [ctx]); + dispatch(documentDimensionsSwapped()); + }, [dispatch]); return ( { - const { widthComponent, heightComponent, previewComponent, ...ctx } = props; - return ( - - - - - - - - - - - {widthComponent} - {heightComponent} - - - - {previewComponent} - - - - ); -}); - -ImageSize.displayName = 'ImageSize'; - -const formLabelProps: FormLabelProps = { - 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 deleted file mode 100644 index fb2a3d9eeb..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -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; - setOptimalSize: () => void; -}; - -export const ImageSizeContext = createContext(null); - -export const useImageSizeContext = (): ImageSizeContext => { - const _ctx = useContext(ImageSizeContext); - const optimalDimension = useAppSelector(selectOptimalDimension); - - 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 setOptimalSize = useCallback(() => { - if (_ctx.aspectRatioState.isLocked) { - const { width, height } = calculateNewSize(_ctx.aspectRatioState.value, optimalDimension * optimalDimension); - _ctx.onChangeWidth(width); - _ctx.onChangeHeight(height); - } else { - _ctx.onChangeAspectRatioState({ ...initialAspectRatioState }); - _ctx.onChangeWidth(optimalDimension); - _ctx.onChangeHeight(optimalDimension); - } - }, [_ctx, optimalDimension]); - - const ctx = useMemo( - () => ({ - ..._ctx, - aspectRatioSelected, - dimensionsSwapped, - widthChanged, - heightChanged, - isLockedToggled, - setOptimalSize, - }), - [_ctx, aspectRatioSelected, dimensionsSwapped, heightChanged, isLockedToggled, setOptimalSize, widthChanged] - ); - - return ctx; -}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 1518e0ec2d..866a20fb2d 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -9,6 +9,7 @@ import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/In import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight'; import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth'; import ParamImageToImageStrength from 'features/parameters/components/Canvas/ParamImageToImageStrength'; +import { DocumentSize } from 'features/parameters/components/DocumentSize/DocumentSize'; import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; @@ -17,7 +18,6 @@ import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { ImageSizeLinear } from './ImageSizeLinear'; const selector = createMemoizedSelector([selectHrfSlice, selectCanvasV2Slice], (hrf, canvasV2) => { const { shouldRandomizeSeed, model } = canvasV2.params; @@ -68,7 +68,7 @@ export const ImageSettingsAccordion = memo(() => { > - + diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx deleted file mode 100644 index 7c24c3d79b..0000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { aspectRatioChanged, heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { ParamHeight } from 'features/parameters/components/Core/ParamHeight'; -import { ParamWidth } from 'features/parameters/components/Core/ParamWidth'; -import { AspectRatioCanvasPreview } from 'features/parameters/components/ImageSize/AspectRatioCanvasPreview'; -import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { memo, useCallback } from 'react'; - -export const ImageSizeLinear = memo(() => { - const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.document.width); - const height = useAppSelector((s) => s.canvasV2.document.height); - const aspectRatioState = useAppSelector((s) => s.canvasV2.document.aspectRatio); - - const onChangeWidth = useCallback( - (width: number) => { - if (width === 0) { - return; - } - dispatch(widthChanged({ width })); - }, - [dispatch] - ); - - const onChangeHeight = useCallback( - (height: number) => { - if (height === 0) { - return; - } - dispatch(heightChanged({ height })); - }, - [dispatch] - ); - - const onChangeAspectRatioState = useCallback( - (aspectRatioState: AspectRatioState) => { - dispatch(aspectRatioChanged(aspectRatioState)); - }, - [dispatch] - ); - - return ( - } - widthComponent={} - previewComponent={} - onChangeAspectRatioState={onChangeAspectRatioState} - onChangeWidth={onChangeWidth} - onChangeHeight={onChangeHeight} - /> - ); -}); - -ImageSizeLinear.displayName = 'ImageSizeLinear';