refactor(ui): remove modular imagesize components

This is no longer necessary with canvas v2 and added a ton of extraneous redux actions when changing the image size. Also renamed to document size
This commit is contained in:
psychedelicious 2024-07-08 18:53:45 +10:00
parent a181a684f5
commit 36e94af598
25 changed files with 241 additions and 371 deletions

View File

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

View File

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

View File

@ -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]);

View File

@ -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]);

View File

@ -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<AspectRatioState>) => {
state.document.aspectRatio = action.payload;
},
...documentReducers,
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
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,

View File

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

View File

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

View File

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

View File

@ -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<ParameterScheduler> = (scheduler) => {
const setSizeOptions = { updateAspectRatio: true, clamp: true };
const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => {
getStore().dispatch(widthChanged({ width, ...setSizeOptions }));
getStore().dispatch(documentWidthChanged({ width, ...setSizeOptions }));
};
const recallHeight: MetadataRecallFunc<ParameterHeight> = (height) => {
getStore().dispatch(heightChanged({ height, ...setSizeOptions }));
getStore().dispatch(documentHeightChanged({ height, ...setSizeOptions }));
};
const recallSteps: MetadataRecallFunc<ParameterSteps> = (steps) => {

View File

@ -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(() => {
<FormLabel>{t('parameters.height')}</FormLabel>
</InformationalPopover>
<CompositeSlider
value={ctx.height}
value={height}
defaultValue={optimalDimension}
onChange={onChange}
min={sliderMin}
@ -42,7 +43,7 @@ export const ParamHeight = memo(() => {
marks={marks}
/>
<CompositeNumberInput
value={ctx.height}
value={height}
defaultValue={optimalDimension}
onChange={onChange}
min={numberInputMin}

View File

@ -1,14 +1,15 @@
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 { documentWidthChanged } 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 ParamWidth = memo(() => {
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(() => {
<FormLabel>{t('parameters.width')}</FormLabel>
</InformationalPopover>
<CompositeSlider
value={ctx.width}
value={width}
onChange={onChange}
defaultValue={optimalDimension}
min={sliderMin}
@ -42,7 +43,7 @@ export const ParamWidth = memo(() => {
marks={marks}
/>
<CompositeNumberInput
value={ctx.width}
value={width}
onChange={onChange}
defaultValue={optimalDimension}
min={numberInputMin}

View File

@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview';
import { AspectRatioIconPreview } from 'features/parameters/components/DocumentSize/AspectRatioIconPreview';
import { memo } from 'react';
export const AspectRatioCanvasPreview = memo(() => {

View File

@ -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<HTMLDivElement>(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 (
<Flex w="full" h="full" alignItems="center" justifyContent="center" ref={containerRef}>

View File

@ -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<ComboboxOption>) => {
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 (
<FormControl>

View File

@ -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 (
<Flex gap={4} alignItems="center">
<Flex gap={4} flexDirection="column" width="full">
<FormControlGroup formLabelProps={formLabelProps}>
<Flex gap={4}>
<AspectRatioSelect />
<SwapDimensionsButton />
<LockAspectRatioButton />
<SetOptimalSizeButton />
</Flex>
<ParamWidth />
<ParamHeight />
</FormControlGroup>
</Flex>
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0}>
<AspectRatioIconPreview />
</Flex>
</Flex>
);
});
DocumentSize.displayName = 'DocumentSize';
const formLabelProps: FormLabelProps = {
minW: 14,
};

View File

@ -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 (
<IconButton
tooltip={t('parameters.lockAspectRatio')}
aria-label={t('parameters.lockAspectRatio')}
onClick={onClick}
variant={ctx.aspectRatioState.isLocked ? 'outline' : 'ghost'}
variant={isLocked ? 'outline' : 'ghost'}
size="sm"
icon={ctx.aspectRatioState.isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
icon={isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
/>
);
});

View File

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

View File

@ -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 (
<IconButton
tooltip={t('parameters.swapDimensions')}

View File

@ -1,47 +0,0 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
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;
previewComponent: ReactNode;
};
export const ImageSize = memo((props: ImageSizeProps) => {
const { widthComponent, heightComponent, previewComponent, ...ctx } = props;
return (
<ImageSizeContext.Provider value={ctx}>
<Flex gap={4} alignItems="center">
<Flex gap={4} flexDirection="column" width="full">
<FormControlGroup formLabelProps={formLabelProps}>
<Flex gap={4}>
<AspectRatioSelect />
<SwapDimensionsButton />
<LockAspectRatioButton />
<SetOptimalSizeButton />
</Flex>
{widthComponent}
{heightComponent}
</FormControlGroup>
</Flex>
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0}>
{previewComponent}
</Flex>
</Flex>
</ImageSizeContext.Provider>
);
});
ImageSize.displayName = 'ImageSize';
const formLabelProps: FormLabelProps = {
minW: 14,
};

View File

@ -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<ImageSizeContextInnerValue | null>(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;
};

View File

@ -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(() => {
>
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
<Flex flexDir="column" gap={4}>
<ImageSizeLinear />
<DocumentSize />
<ParamImageToImageStrength />
</Flex>
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>

View File

@ -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 (
<ImageSize
width={width}
height={height}
aspectRatioState={aspectRatioState}
heightComponent={<ParamHeight />}
widthComponent={<ParamWidth />}
previewComponent={<AspectRatioCanvasPreview />}
onChangeAspectRatioState={onChangeAspectRatioState}
onChangeWidth={onChangeWidth}
onChangeHeight={onChangeHeight}
/>
);
});
ImageSizeLinear.displayName = 'ImageSizeLinear';