fix(ui): canvas staging area & batch handling fixes

Handful of intertwined fixes.

- Create and use helper function to reset staging area.
- Clear staging area when queue items are canceled, failed, cleared, etc. Fixes a bug where the bbox ends up offset and images are put into the wrong spot.
- Fix a number of similar bugs where canvas would "forget" it had pending generations, but they continued to generate. Canvas needs to track batches that should be displayed in it using `state.canvas.batchIds`, and this was getting cleared without actually canceling those batches.
- Disable the `discard current image` button on canvas if there is only one image. Prevents accidentally canceling all canvas batches if you spam the button.
This commit is contained in:
psychedelicious 2024-04-10 10:00:07 +10:00
parent c0d54d5414
commit 7e2ade50e1
3 changed files with 40 additions and 39 deletions

View File

@ -1,12 +1,18 @@
import { isAnyOf } from '@reduxjs/toolkit'; import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasBatchIdsReset, commitStagingAreaImage, discardStagedImages } from 'features/canvas/store/canvasSlice'; import {
canvasBatchIdsReset,
commitStagingAreaImage,
discardStagedImages,
resetCanvas,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue'; import { queueApi } from 'services/api/endpoints/queue';
const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages); const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages, resetCanvas, setInitialCanvasImage);
export const addCommitStagingAreaImageListener = (startAppListening: AppStartListening) => { export const addCommitStagingAreaImageListener = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({

View File

@ -49,14 +49,20 @@ const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
const ClearStagingIntermediatesIconButton = () => { const ClearStagingIntermediatesIconButton = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const totalStagedImages = useAppSelector((s) => s.canvas.layerState.stagingArea.images.length);
const handleDiscardStagingArea = useCallback(() => { const handleDiscardStagingArea = useCallback(() => {
dispatch(discardStagedImages()); dispatch(discardStagedImages());
}, [dispatch]); }, [dispatch]);
const handleDiscardStagingImage = useCallback(() => { const handleDiscardStagingImage = useCallback(() => {
dispatch(discardStagedImage()); // Discarding all staged images triggers cancelation of all canvas batches. It's too easy to accidentally
}, [dispatch]); // click the discard button, so to prevent accidental cancelation of all batches, we only discard the current
// image if there are more than one staged images.
if (totalStagedImages > 1) {
dispatch(discardStagedImage());
}
}, [dispatch, totalStagedImages]);
return ( return (
<> <>
@ -67,6 +73,7 @@ const ClearStagingIntermediatesIconButton = () => {
onClick={handleDiscardStagingImage} onClick={handleDiscardStagingImage}
colorScheme="invokeBlue" colorScheme="invokeBlue"
fontSize={16} fontSize={16}
isDisabled={totalStagedImages <= 1}
/> />
<IconButton <IconButton
tooltip={`${t('unifiedCanvas.discardAll')} (Esc)`} tooltip={`${t('unifiedCanvas.discardAll')} (Esc)`}

View File

@ -190,7 +190,6 @@ export const canvasSlice = createSlice({
], ],
}; };
state.futureLayerStates = []; state.futureLayerStates = [];
state.batchIds = [];
const newScale = calculateScale( const newScale = calculateScale(
stageDimensions.width, stageDimensions.width,
@ -286,40 +285,14 @@ export const canvasSlice = createSlice({
}, },
discardStagedImages: (state) => { discardStagedImages: (state) => {
pushToPrevLayerStates(state); pushToPrevLayerStates(state);
resetStagingArea(state);
state.layerState.stagingArea = deepClone(initialLayerState.stagingArea);
state.futureLayerStates = []; state.futureLayerStates = [];
state.shouldShowStagingOutline = true;
state.shouldShowStagingImage = true;
state.batchIds = [];
}, },
discardStagedImage: (state) => { discardStagedImage: (state) => {
const { images, selectedImageIndex } = state.layerState.stagingArea; const { images, selectedImageIndex } = state.layerState.stagingArea;
pushToPrevLayerStates(state); pushToPrevLayerStates(state);
images.splice(selectedImageIndex, 1); images.splice(selectedImageIndex, 1);
state.layerState.stagingArea.selectedImageIndex = Math.max(0, images.length - 1);
if (images.length === 0) {
pushToPrevLayerStates(state);
state.layerState.stagingArea = deepClone(initialLayerState.stagingArea);
state.futureLayerStates = [];
state.shouldShowStagingOutline = true;
state.shouldShowStagingImage = true;
state.batchIds = [];
}
if (selectedImageIndex >= images.length) {
state.layerState.stagingArea.selectedImageIndex = images.length - 1;
}
if (!images.length) {
state.shouldShowStagingImage = false;
state.shouldShowStagingOutline = false;
}
state.futureLayerStates = []; state.futureLayerStates = [];
}, },
addFillRect: (state) => { addFillRect: (state) => {
@ -433,7 +406,6 @@ export const canvasSlice = createSlice({
pushToPrevLayerStates(state); pushToPrevLayerStates(state);
state.layerState = deepClone(initialLayerState); state.layerState = deepClone(initialLayerState);
state.futureLayerStates = []; state.futureLayerStates = [];
state.batchIds = [];
state.boundingBoxCoordinates = { state.boundingBoxCoordinates = {
...initialCanvasState.boundingBoxCoordinates, ...initialCanvasState.boundingBoxCoordinates,
}; };
@ -534,12 +506,9 @@ export const canvasSlice = createSlice({
...imageToCommit, ...imageToCommit,
}); });
} }
state.layerState.stagingArea = deepClone(initialLayerState.stagingArea);
resetStagingArea(state);
state.futureLayerStates = []; state.futureLayerStates = [];
state.shouldShowStagingOutline = true;
state.shouldShowStagingImage = true;
state.batchIds = [];
}, },
setBoundingBoxScaleMethod: { setBoundingBoxScaleMethod: {
reducer: (state, action: PayloadActionWithOptimalDimension<BoundingBoxScaleMethod>) => { reducer: (state, action: PayloadActionWithOptimalDimension<BoundingBoxScaleMethod>) => {
@ -647,12 +616,19 @@ export const canvasSlice = createSlice({
if (batch_status.in_progress === 0 && batch_status.pending === 0) { if (batch_status.in_progress === 0 && batch_status.pending === 0) {
state.batchIds = state.batchIds.filter((id) => id !== batch_status.batch_id); state.batchIds = state.batchIds.filter((id) => id !== batch_status.batch_id);
} }
const queueItemStatus = action.payload.data.queue_item.status;
if (queueItemStatus === 'canceled' || queueItemStatus === 'failed') {
resetStagingAreaIfEmpty(state);
}
}); });
builder.addMatcher(queueApi.endpoints.clearQueue.matchFulfilled, (state) => { builder.addMatcher(queueApi.endpoints.clearQueue.matchFulfilled, (state) => {
state.batchIds = []; state.batchIds = [];
resetStagingAreaIfEmpty(state);
}); });
builder.addMatcher(queueApi.endpoints.cancelByBatchIds.matchFulfilled, (state, action) => { builder.addMatcher(queueApi.endpoints.cancelByBatchIds.matchFulfilled, (state, action) => {
state.batchIds = state.batchIds.filter((id) => !action.meta.arg.originalArgs.batch_ids.includes(id)); state.batchIds = state.batchIds.filter((id) => !action.meta.arg.originalArgs.batch_ids.includes(id));
resetStagingAreaIfEmpty(state);
}); });
}, },
}); });
@ -726,7 +702,7 @@ export const canvasPersistConfig: PersistConfig<CanvasState> = {
name: canvasSlice.name, name: canvasSlice.name,
initialState: initialCanvasState, initialState: initialCanvasState,
migrate: migrateCanvasState, migrate: migrateCanvasState,
persistDenylist: [], persistDenylist: ['shouldShowStagingImage', 'shouldShowStagingOutline'],
}; };
const pushToPrevLayerStates = (state: CanvasState) => { const pushToPrevLayerStates = (state: CanvasState) => {
@ -742,3 +718,15 @@ const pushToFutureLayerStates = (state: CanvasState) => {
state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY); state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY);
} }
}; };
const resetStagingAreaIfEmpty = (state: CanvasState) => {
if (state.batchIds.length === 0 && state.layerState.stagingArea.images.length === 0) {
resetStagingArea(state);
}
};
const resetStagingArea = (state: CanvasState) => {
state.layerState.stagingArea = { ...initialCanvasState.layerState.stagingArea };
state.shouldShowStagingImage = initialCanvasState.shouldShowStagingImage;
state.shouldShowStagingOutline = initialCanvasState.shouldShowStagingOutline;
};