Skip to content

Commit 95425fa

Browse files
author
Attila Cseh
committed
undo/redo handles multiple canvases
1 parent dcce85f commit 95425fa

File tree

11 files changed

+510
-545
lines changed

11 files changed

+510
-545
lines changed

invokeai/frontend/web/src/app/store/store.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ import { merge } from 'es-toolkit';
2222
import { omit, pick } from 'es-toolkit/compat';
2323
import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice';
2424
import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice';
25-
import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice';
25+
import { canvasSliceConfig, undoableCanvasesReducer } from 'features/controlLayers/store/canvasSlice';
2626
import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice';
2727
import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice';
2828
import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice';
2929
import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice';
3030
import { dynamicPromptsSliceConfig } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
3131
import { gallerySliceConfig } from 'features/gallery/store/gallerySlice';
3232
import { modelManagerSliceConfig } from 'features/modelManagerV2/store/modelManagerV2Slice';
33-
import { nodesSliceConfig } from 'features/nodes/store/nodesSlice';
33+
import { nodesSliceConfig, undoableNodesSliceReducer } from 'features/nodes/store/nodesSlice';
3434
import { workflowLibrarySliceConfig } from 'features/nodes/store/workflowLibrarySlice';
3535
import { workflowSettingsSliceConfig } from 'features/nodes/store/workflowSettingsSlice';
3636
import { upscaleSliceConfig } from 'features/parameters/store/upscaleSlice';
@@ -44,7 +44,6 @@ import { diff } from 'jsondiffpatch';
4444
import dynamicMiddlewares from 'redux-dynamic-middlewares';
4545
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
4646
import { REMEMBER_REHYDRATED, rememberEnhancer, rememberReducer } from 'redux-remember';
47-
import undoable, { newHistory } from 'redux-undo';
4847
import { serializeError } from 'serialize-error';
4948
import { api } from 'services/api';
5049
import { authToastMiddleware } from 'services/api/authToastMiddleware';
@@ -91,22 +90,14 @@ const ALL_REDUCERS = {
9190
[api.reducerPath]: api.reducer,
9291
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer,
9392
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer,
94-
// Undoable!
95-
[canvasSliceConfig.slice.reducerPath]: undoable(
96-
canvasSliceConfig.slice.reducer,
97-
canvasSliceConfig.undoableConfig?.reduxUndoOptions
98-
),
93+
[canvasSliceConfig.slice.reducerPath]: undoableCanvasesReducer,
9994
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer,
10095
[configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer,
10196
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer,
10297
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer,
10398
[lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer,
10499
[modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer,
105-
// Undoable!
106-
[nodesSliceConfig.slice.reducerPath]: undoable(
107-
nodesSliceConfig.slice.reducer,
108-
nodesSliceConfig.undoableConfig?.reduxUndoOptions
109-
),
100+
[nodesSliceConfig.slice.reducerPath]: undoableNodesSliceReducer,
110101
[paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer,
111102
[queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer,
112103
[refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer,
@@ -128,7 +119,7 @@ const unserialize: UnserializeFunction = (data, key) => {
128119
if (!sliceConfig?.persistConfig) {
129120
throw new Error(`No persist config for slice "${key}"`);
130121
}
131-
const { getInitialState, persistConfig, undoableConfig } = sliceConfig;
122+
const { getInitialState, persistConfig } = sliceConfig;
132123
let state;
133124
try {
134125
const initialState = getInitialState();
@@ -160,12 +151,7 @@ const unserialize: UnserializeFunction = (data, key) => {
160151
state = getInitialState();
161152
}
162153

163-
// Undoable slices must be wrapped in a history!
164-
if (undoableConfig) {
165-
return newHistory([], state, []);
166-
} else {
167-
return state;
168-
}
154+
return persistConfig.wrapState ? persistConfig.wrapState(state) : state;
169155
};
170156

171157
const serialize: SerializeFunction = (data, key) => {
@@ -175,7 +161,7 @@ const serialize: SerializeFunction = (data, key) => {
175161
}
176162

177163
const result = omit(
178-
sliceConfig.undoableConfig ? data.present : data,
164+
sliceConfig.persistConfig.unwrapState ? sliceConfig.persistConfig.unwrapState(data) : data,
179165
sliceConfig.persistConfig.persistDenylist ?? []
180166
);
181167

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import type { Slice } from '@reduxjs/toolkit';
2-
import type { UndoableOptions } from 'redux-undo';
32
import type { ZodType } from 'zod';
43

54
type StateFromSlice<T extends Slice> = T extends Slice<infer U> ? U : never;
65

7-
export type SliceConfig<T extends Slice> = {
6+
export type SliceConfig<T extends Slice, TInternalState = StateFromSlice<T>, TSerializedState = StateFromSlice<T>> = {
87
/**
98
* The redux slice (return of createSlice).
109
*/
@@ -16,7 +15,7 @@ export type SliceConfig<T extends Slice> = {
1615
/**
1716
* A function that returns the initial state of the slice.
1817
*/
19-
getInitialState: () => StateFromSlice<T>;
18+
getInitialState: () => TSerializedState;
2019
/**
2120
* The optional persist configuration for this slice. If omitted, the slice will not be persisted.
2221
*/
@@ -28,19 +27,24 @@ export type SliceConfig<T extends Slice> = {
2827
* @param state The rehydrated state.
2928
* @returns A correctly-shaped state.
3029
*/
31-
migrate: (state: unknown) => StateFromSlice<T>;
30+
migrate: (state: unknown) => TSerializedState;
3231
/**
3332
* Keys to omit from the persisted state.
3433
*/
3534
persistDenylist?: (keyof StateFromSlice<T>)[];
36-
};
37-
/**
38-
* The optional undoable configuration for this slice. If omitted, the slice will not be undoable.
39-
*/
40-
undoableConfig?: {
4135
/**
42-
* The options to be passed into redux-undo.
36+
* Wraps state into state with history
37+
*
38+
* @param state The state without history
39+
* @returns The state with history
40+
*/
41+
wrapState?: (state: unknown) => TInternalState;
42+
/**
43+
* Unwraps state with history
44+
*
45+
* @param state The state with history
46+
* @returns The state without history
4347
*/
44-
reduxUndoOptions: UndoableOptions<StateFromSlice<T>>;
48+
unwrapState?: (state: TInternalState) => TSerializedState;
4549
};
4650
};

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
rasterLayerAdjustmentsReset,
1414
rasterLayerAdjustmentsSet,
1515
} from 'features/controlLayers/store/canvasSlice';
16-
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
16+
import { selectEntity, selectSelectedCanvas } from 'features/controlLayers/store/selectors';
1717
import React, { memo, useCallback, useMemo } from 'react';
1818
import { useTranslation } from 'react-i18next';
1919
import { PiArrowCounterClockwiseBold, PiCaretDownBold, PiCheckBold, PiTrashBold } from 'react-icons/pi';
@@ -25,30 +25,32 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
2525
const canvasManager = useCanvasManager();
2626

2727
const selectHasAdjustments = useMemo(() => {
28-
return createSelector(selectCanvasSlice, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)?.adjustments));
28+
return createSelector(selectSelectedCanvas, (canvas) =>
29+
Boolean(selectEntity(canvas, entityIdentifier)?.adjustments)
30+
);
2931
}, [entityIdentifier]);
3032

3133
const hasAdjustments = useAppSelector(selectHasAdjustments);
3234

3335
const selectMode = useMemo(() => {
3436
return createSelector(
35-
selectCanvasSlice,
37+
selectSelectedCanvas,
3638
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.mode ?? 'simple'
3739
);
3840
}, [entityIdentifier]);
3941
const mode = useAppSelector(selectMode);
4042

4143
const selectEnabled = useMemo(() => {
4244
return createSelector(
43-
selectCanvasSlice,
45+
selectSelectedCanvas,
4446
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled ?? false
4547
);
4648
}, [entityIdentifier]);
4749
const enabled = useAppSelector(selectEnabled);
4850

4951
const selectCollapsed = useMemo(() => {
5052
return createSelector(
51-
selectCanvasSlice,
53+
selectSelectedCanvas,
5254
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.collapsed ?? false
5355
);
5456
}, [entityIdentifier]);

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCurvesAdjustmentsEditor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
55
import { useEntityAdapterContext } from 'features/controlLayers/contexts/EntityAdapterContext';
66
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
77
import { rasterLayerAdjustmentsCurvesUpdated } from 'features/controlLayers/store/canvasSlice';
8-
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
8+
import { selectEntity, selectSelectedCanvas } from 'features/controlLayers/store/selectors';
99
import type { ChannelName, ChannelPoints, CurvesAdjustmentsConfig } from 'features/controlLayers/store/types';
1010
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
1111
import { useTranslation } from 'react-i18next';
@@ -72,15 +72,15 @@ export const RasterLayerCurvesAdjustmentsEditor = memo(() => {
7272
const { t } = useTranslation();
7373
const selectCurves = useMemo(() => {
7474
return createSelector(
75-
selectCanvasSlice,
75+
selectSelectedCanvas,
7676
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.curves ?? DEFAULT_CURVES
7777
);
7878
}, [entityIdentifier]);
7979
const curves = useAppSelector(selectCurves);
8080

8181
const selectIsDisabled = useMemo(() => {
8282
return createSelector(
83-
selectCanvasSlice,
83+
selectSelectedCanvas,
8484
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled !== true
8585
);
8686
}, [entityIdentifier]);

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import { MenuItem } from '@invoke-ai/ui-library';
2+
import { createSelector } from '@reduxjs/toolkit';
23
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
34
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
45
import { rasterLayerAdjustmentsCancel, rasterLayerAdjustmentsSet } from 'features/controlLayers/store/canvasSlice';
6+
import { selectSelectedCanvas } from 'features/controlLayers/store/selectors';
57
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
68
import { makeDefaultRasterLayerAdjustments } from 'features/controlLayers/store/util';
7-
import { memo, useCallback } from 'react';
9+
import { memo, useCallback, useMemo } from 'react';
810
import { useTranslation } from 'react-i18next';
911
import { PiSlidersHorizontalBold } from 'react-icons/pi';
1012

1113
export const RasterLayerMenuItemsAdjustments = memo(() => {
1214
const dispatch = useAppDispatch();
1315
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
1416
const { t } = useTranslation();
15-
const layer = useAppSelector((s) =>
16-
s.canvas.present.rasterLayers.entities.find((e: CanvasRasterLayerState) => e.id === entityIdentifier.id)
17-
);
17+
const selectRasterLayer = useMemo(() => {
18+
return createSelector(selectSelectedCanvas, (canvas) =>
19+
canvas.rasterLayers.entities.find((e: CanvasRasterLayerState) => e.id === entityIdentifier.id)
20+
);
21+
}, [entityIdentifier]);
22+
const layer = useAppSelector(selectRasterLayer);
1823
const hasAdjustments = Boolean(layer?.adjustments);
1924
const onToggleAdjustmentsPresence = useCallback(() => {
2025
if (hasAdjustments) {

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerSimpleAdjustmentsEditor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
33
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
44
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
55
import { rasterLayerAdjustmentsSimpleUpdated } from 'features/controlLayers/store/canvasSlice';
6-
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
6+
import { selectEntity, selectSelectedCanvas } from 'features/controlLayers/store/selectors';
77
import type { SimpleAdjustmentsConfig } from 'features/controlLayers/store/types';
88
import React, { memo, useCallback, useMemo } from 'react';
99
import { useTranslation } from 'react-i18next';
@@ -21,7 +21,7 @@ const AdjustmentSliderRow = ({ label, name, onChange, min = -1, max = 1, step =
2121
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
2222
const selectValue = useMemo(() => {
2323
return createSelector(
24-
selectCanvasSlice,
24+
selectSelectedCanvas,
2525
(canvas) =>
2626
selectEntity(canvas, entityIdentifier)?.adjustments?.simple?.[name] ?? DEFAULT_SIMPLE_ADJUSTMENTS[name]
2727
);
@@ -54,7 +54,7 @@ export const RasterLayerSimpleAdjustmentsEditor = memo(() => {
5454
const { t } = useTranslation();
5555
const selectIsDisabled = useMemo(() => {
5656
return createSelector(
57-
selectCanvasSlice,
57+
selectSelectedCanvas,
5858
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled !== true
5959
);
6060
}, [entityIdentifier]);

0 commit comments

Comments
 (0)