Skip to content

Commit b4f8e4b

Browse files
feat(ui): lock down bbox while staging
1 parent 049d878 commit b4f8e4b

File tree

16 files changed

+92
-47
lines changed

16 files changed

+92
-47
lines changed

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { logger } from 'app/logging/logger';
22
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
3+
import { bboxOptimalDimensionChanged, bboxSyncedToOptimalDimension } from 'features/controlLayers/store/canvasSlice';
4+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
35
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
46
import { modelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
57
import { modelSelected } from 'features/parameters/store/actions';
68
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
9+
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
710
import { toast } from 'features/toast/toast';
811
import { t } from 'i18next';
912

@@ -68,6 +71,11 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
6871
}
6972

7073
dispatch(modelChanged({ model: newModel, previousModel: state.params.model }));
74+
// When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync.
75+
dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(newModel) }));
76+
if (!selectIsStaging(state)) {
77+
dispatch(bboxSyncedToOptimalDimension());
78+
}
7179
},
7280
});
7381
};

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
33
import type { AppDispatch, RootState } from 'app/store/store';
44
import type { SerializableObject } from 'common/types';
55
import {
6-
bboxHeightChanged,
7-
bboxWidthChanged,
6+
bboxOptimalDimensionChanged,
7+
bboxSyncedToOptimalDimension,
88
controlLayerModelChanged,
99
referenceImageIPAdapterModelChanged,
1010
rgIPAdapterModelChanged,
1111
} from 'features/controlLayers/store/canvasSlice';
12+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
1213
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
1314
import {
1415
clipEmbedModelSelected,
@@ -20,10 +21,9 @@ import {
2021
} from 'features/controlLayers/store/paramsSlice';
2122
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
2223
import { getEntityIdentifier } from 'features/controlLayers/store/types';
23-
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
2424
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
2525
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
26-
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
26+
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
2727
import type { Logger } from 'roarr';
2828
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
2929
import type { AnyModelConfig } from 'services/api/types';
@@ -95,15 +95,11 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
9595
const result = zParameterModel.safeParse(defaultModelInList);
9696
if (result.success) {
9797
dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel }));
98-
const { bbox } = selectCanvasSlice(state);
99-
const optimalDimension = getOptimalDimension(defaultModelInList);
100-
if (getIsSizeOptimal(bbox.rect.width, bbox.rect.height, optimalDimension)) {
101-
return;
98+
// When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync.
99+
dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(defaultModelInList) }));
100+
if (!selectIsStaging(state)) {
101+
dispatch(bboxSyncedToOptimalDimension());
102102
}
103-
const { width, height } = calculateNewSize(bbox.aspectRatio.value, optimalDimension * optimalDimension);
104-
105-
dispatch(bboxWidthChanged({ width }));
106-
dispatch(bboxHeightChanged({ height }));
107103
return;
108104
}
109105
}
@@ -116,6 +112,11 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
116112
}
117113

118114
dispatch(modelChanged({ model: result.data, previousModel: currentModel }));
115+
// When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync.
116+
dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(result.data) }));
117+
if (!selectIsStaging(state)) {
118+
dispatch(bboxSyncedToOptimalDimension());
119+
}
119120
};
120121

121122
const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => {

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
22
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
3+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
34
import {
45
setCfgRescaleMultiplier,
56
setCfgScale,
@@ -96,13 +97,15 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
9697
}
9798
}
9899
const setSizeOptions = { updateAspectRatio: true, clamp: true };
99-
if (width) {
100+
101+
const isStaging = selectIsStaging(getState());
102+
if (!isStaging && width) {
100103
if (isParameterWidth(width)) {
101104
dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
102105
}
103106
}
104107

105-
if (height) {
108+
if (!isStaging && height) {
106109
if (isParameterHeight(height)) {
107110
dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
108111
}

invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import IAIDndImage from 'common/components/IAIDndImage';
66
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
77
import { useNanoid } from 'common/hooks/useNanoid';
88
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
9+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
910
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
1011
import type { ImageWithDims } from 'features/controlLayers/store/types';
1112
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
@@ -27,6 +28,7 @@ type Props = {
2728
export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => {
2829
const { t } = useTranslation();
2930
const dispatch = useAppDispatch();
31+
const isStaging = useAppSelector(selectIsStaging);
3032
const isConnected = useStore($isConnected);
3133
const optimalDimension = useAppSelector(selectOptimalDimension);
3234
const shift = useShiftModifier();
@@ -95,6 +97,7 @@ export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData
9597
onClick={handleSetControlImageToDimensions}
9698
icon={<PiRulerBold size={16} />}
9799
tooltip={shift ? t('controlLayers.useSizeIgnoreModel') : t('controlLayers.useSizeOptimizeForModel')}
100+
isDisabled={isStaging}
98101
/>
99102
</Flex>
100103
)}

invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { deepClone } from 'common/util/deepClone';
66
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
77
import { getPrefixedId } from 'features/controlLayers/konva/util';
88
import { canvasReset } from 'features/controlLayers/store/actions';
9-
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
109
import {
1110
selectAllEntities,
1211
selectAllEntitiesOfType,
@@ -25,7 +24,7 @@ import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
2524
import { zModelIdentifierField } from 'features/nodes/types/common';
2625
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
2726
import { ASPECT_RATIO_MAP } from 'features/parameters/components/Bbox/constants';
28-
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
27+
import { getIsSizeOptimal } from 'features/parameters/util/optimalDimension';
2928
import type { IRect } from 'konva/lib/types';
3029
import { merge, omit } from 'lodash-es';
3130
import type { UndoableOptions } from 'redux-undo';
@@ -749,6 +748,22 @@ export const canvasSlice = createSlice({
749748

750749
syncScaledSize(state);
751750
},
751+
bboxOptimalDimensionChanged: (state, action: PayloadAction<{ optimalDimension: number }>) => {
752+
// When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync.
753+
// This action does the syncing. `bboxSyncedToOptimalDimension` below will actually change the bbox,
754+
// and is only called when we are not staging.
755+
const { optimalDimension } = action.payload;
756+
state.bbox.optimalDimension = optimalDimension;
757+
},
758+
bboxSyncedToOptimalDimension: (state) => {
759+
const { optimalDimension } = state.bbox;
760+
if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) {
761+
const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension);
762+
state.bbox.rect.width = bboxDims.width;
763+
state.bbox.rect.height = bboxDims.height;
764+
syncScaledSize(state);
765+
}
766+
},
752767
//#region Shared entity
753768
entitySelected: (state, action: PayloadAction<EntityIdentifierPayload>) => {
754769
const { entityIdentifier } = action.payload;
@@ -1052,27 +1067,6 @@ export const canvasSlice = createSlice({
10521067
canvasClearHistory: () => {},
10531068
},
10541069
extraReducers(builder) {
1055-
builder.addCase(modelChanged, (state, action) => {
1056-
const { model, previousModel } = action.payload;
1057-
1058-
// If the model base changes (e.g. SD1.5 -> SDXL), we need to change a few things
1059-
if (model === null || previousModel?.base === model.base) {
1060-
return;
1061-
}
1062-
1063-
// Update the bbox size to match the new model's optimal size
1064-
const optimalDimension = getOptimalDimension(model);
1065-
1066-
state.bbox.optimalDimension = optimalDimension;
1067-
1068-
if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) {
1069-
const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension);
1070-
state.bbox.rect.width = bboxDims.width;
1071-
state.bbox.rect.height = bboxDims.height;
1072-
syncScaledSize(state);
1073-
}
1074-
});
1075-
10761070
builder.addCase(canvasReset, (state) => {
10771071
return resetState(state);
10781072
});
@@ -1135,6 +1129,8 @@ export const {
11351129
bboxAspectRatioIdChanged,
11361130
bboxDimensionsSwapped,
11371131
bboxSizeOptimized,
1132+
bboxOptimalDimensionChanged,
1133+
bboxSyncedToOptimalDimension,
11381134
// Raster layers
11391135
rasterLayerAdded,
11401136
// rasterLayerRecalled,
@@ -1215,6 +1211,11 @@ export const canvasUndoableConfig: UndoableOptions<CanvasState, UnknownAction> =
12151211
if (!action.type.startsWith(canvasSlice.name)) {
12161212
return false;
12171213
}
1214+
if (bboxOptimalDimensionChanged.match(action)) {
1215+
// This action is not triggered by the user. it's dispatched when the model is changed and will have no visible
1216+
// effect on the canvas.
1217+
return false;
1218+
}
12181219
// Throttle rapid actions of the same type
12191220
filter = actionsThrottlingFilter(action);
12201221
return filter;

invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
44
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
55
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
66
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
7+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
78
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
89
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
910
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
@@ -34,6 +35,7 @@ import { $isConnected, $progressImage } from 'services/events/stores';
3435
const CurrentImageButtons = () => {
3536
const dispatch = useAppDispatch();
3637
const isConnected = useStore($isConnected);
38+
const isStaging = useAppSelector(selectIsStaging);
3739
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
3840
const progressImage = useStore($progressImage);
3941
const shouldDisableToolbarButtons = useMemo(() => {
@@ -59,8 +61,11 @@ const CurrentImageButtons = () => {
5961
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
6062

6163
const handleUseSize = useCallback(() => {
64+
if (isStaging) {
65+
return;
66+
}
6267
parseAndRecallImageDimensions(lastSelectedImage);
63-
}, [lastSelectedImage]);
68+
}, [isStaging, lastSelectedImage]);
6469
const handleClickUpscale = useCallback(() => {
6570
if (!imageDTO) {
6671
return;
@@ -179,6 +184,7 @@ const CurrentImageButtons = () => {
179184
tooltip={`${t('parameters.useSize')} (D)`}
180185
aria-label={`${t('parameters.useSize')} (D)`}
181186
onClick={handleUseSize}
187+
isDisabled={isStaging}
182188
/>
183189
<IconButton
184190
isLoading={isLoadingMetadata}

invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { skipToken } from '@reduxjs/toolkit/query';
22
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
3+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
34
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
45
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
56
import {
@@ -17,6 +18,7 @@ export const useImageActions = (image_name?: string) => {
1718
const dispatch = useAppDispatch();
1819
const { t } = useTranslation();
1920
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
21+
const isStaging = useAppSelector(selectIsStaging);
2022
const activeTabName = useAppSelector(selectActiveTab);
2123
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(image_name);
2224
const [hasMetadata, setHasMetadata] = useState(false);
@@ -66,9 +68,9 @@ export const useImageActions = (image_name?: string) => {
6668
}, [dispatch, activeStylePresetId, t]);
6769

6870
const recallAll = useCallback(() => {
69-
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas');
71+
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', isStaging ? ['width', 'height'] : []);
7072
clearStylePreset();
71-
}, [activeTabName, metadata, clearStylePreset]);
73+
}, [metadata, activeTabName, isStaging, clearStylePreset]);
7274

7375
const remix = useCallback(() => {
7476
// Recalls all metadata parameters except seed

invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
44
import type { SingleValue } from 'chakra-react-select';
55
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
66
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
7+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
78
import { selectAspectRatioID } from 'features/controlLayers/store/selectors';
89
import { isAspectRatioID } from 'features/controlLayers/store/types';
910
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/Bbox/constants';
@@ -14,6 +15,7 @@ export const BboxAspectRatioSelect = memo(() => {
1415
const { t } = useTranslation();
1516
const dispatch = useAppDispatch();
1617
const id = useAppSelector(selectAspectRatioID);
18+
const isStaging = useAppSelector(selectIsStaging);
1719

1820
const onChange = useCallback(
1921
(v: SingleValue<ComboboxOption>) => {
@@ -28,7 +30,7 @@ export const BboxAspectRatioSelect = memo(() => {
2830
const value = useMemo(() => ASPECT_RATIO_OPTIONS.filter((o) => o.value === id)[0], [id]);
2931

3032
return (
31-
<FormControl>
33+
<FormControl isDisabled={isStaging}>
3234
<InformationalPopover feature="paramAspect">
3335
<FormLabel>{t('parameters.aspect')}</FormLabel>
3436
</InformationalPopover>

invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@
22
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
33
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
44
import { bboxHeightChanged } from 'features/controlLayers/store/canvasSlice';
5+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
56
import { selectHeight, selectOptimalDimension } from 'features/controlLayers/store/selectors';
67
import { selectHeightConfig } from 'features/system/store/configSlice';
78
import { memo, useCallback, useMemo } from 'react';
@@ -13,6 +14,7 @@ export const BboxHeight = memo(() => {
1314
const optimalDimension = useAppSelector(selectOptimalDimension);
1415
const height = useAppSelector(selectHeight);
1516
const config = useAppSelector(selectHeightConfig);
17+
const isStaging = useAppSelector(selectIsStaging);
1618

1719
const onChange = useCallback(
1820
(v: number) => {
@@ -27,7 +29,7 @@ export const BboxHeight = memo(() => {
2729
);
2830

2931
return (
30-
<FormControl>
32+
<FormControl isDisabled={isStaging}>
3133
<InformationalPopover feature="paramHeight">
3234
<FormLabel>{t('parameters.height')}</FormLabel>
3335
</InformationalPopover>

invokeai/frontend/web/src/features/parameters/components/Bbox/BboxLockAspectRatioButton.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
22
import { createSelector } from '@reduxjs/toolkit';
33
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
44
import { bboxAspectRatioLockToggled } from 'features/controlLayers/store/canvasSlice';
5+
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
56
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
67
import { memo, useCallback } from 'react';
78
import { useTranslation } from 'react-i18next';
@@ -13,6 +14,7 @@ export const BboxLockAspectRatioButton = memo(() => {
1314
const { t } = useTranslation();
1415
const dispatch = useAppDispatch();
1516
const isLocked = useAppSelector(selectAspectRatioIsLocked);
17+
const isStaging = useAppSelector(selectIsStaging);
1618
const onClick = useCallback(() => {
1719
dispatch(bboxAspectRatioLockToggled());
1820
}, [dispatch]);
@@ -25,6 +27,7 @@ export const BboxLockAspectRatioButton = memo(() => {
2527
variant={isLocked ? 'outline' : 'ghost'}
2628
size="sm"
2729
icon={isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
30+
isDisabled={isStaging}
2831
/>
2932
);
3033
});

0 commit comments

Comments
 (0)