feat(ui): split out tool state from canvas rendering state

This commit is contained in:
psychedelicious 2024-08-26 21:52:43 +10:00
parent 9c3da8de8e
commit 3ce8294379
14 changed files with 96 additions and 105 deletions

View File

@ -8,6 +8,7 @@ import { deepClone } from 'common/util/deepClone';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
@ -59,6 +60,7 @@ const allReducers = {
[upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.reducer,
[paramsSlice.name]: paramsSlice.reducer,
[toolSlice.name]: toolSlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@ -101,6 +103,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
[paramsPersistConfig.name]: paramsPersistConfig,
[toolPersistConfig.name]: toolPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {

View File

@ -1,6 +1,6 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice';
import { invertScrollChanged } from 'features/controlLayers/store/toolSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
const invertScroll = useAppSelector((s) => s.tool.invertScroll);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
[dispatch]

View File

@ -10,7 +10,7 @@ import {
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { brushWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { brushWidthChanged } from 'features/controlLayers/store/toolSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`;
export const ToolBrushWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const width = useAppSelector((s) => s.canvasV2.tool.brush.width);
const width = useAppSelector((s) => s.tool.brush.width);
const onChange = useCallback(
(v: number) => {
dispatch(brushWidthChanged(Math.round(v)));

View File

@ -10,7 +10,7 @@ import {
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { eraserWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { eraserWidthChanged } from 'features/controlLayers/store/toolSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`;
export const ToolEraserWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const width = useAppSelector((s) => s.canvasV2.tool.eraser.width);
const width = useAppSelector((s) => s.tool.eraser.width);
const onChange = useCallback(
(v: number) => {
dispatch(eraserWidthChanged(Math.round(v)));

View File

@ -2,14 +2,14 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@inv
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { fillChanged } from 'features/controlLayers/store/canvasV2Slice';
import { fillChanged } from 'features/controlLayers/store/toolSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const ToolFillColorPicker = memo(() => {
const { t } = useTranslation();
const fill = useAppSelector((s) => s.canvasV2.tool.fill);
const fill = useAppSelector((s) => s.tool.fill);
const dispatch = useAppDispatch();
const onChange = useCallback(
(color: RgbaColor) => {

View File

@ -11,7 +11,6 @@ import type {
CanvasEntityIdentifier,
CanvasEraserLineState,
CanvasRasterLayerState,
CanvasV2State,
Coordinate,
Rect,
} from 'features/controlLayers/store/types';
@ -83,11 +82,7 @@ export class CanvasLayerAdapter extends CanvasModuleBase {
this.konva.layer.destroy();
};
update = async (arg?: {
state: CanvasLayerAdapter['state'];
toolState: CanvasV2State['tool'];
isSelected: boolean;
}) => {
update = async (arg?: { state: CanvasLayerAdapter['state'] }) => {
const state = get(arg, 'state', this.state);
if (!this.isFirstRender && state === this.state) {

View File

@ -112,7 +112,7 @@ export class CanvasManager extends CanvasModuleBase {
// These atoms require the canvas manager to be set up before we can provide their initial values
this.stateApi.$transformingEntity.set(null);
this.stateApi.$toolState.set(this.stateApi.getToolState());
this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier);
this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier);
this.stateApi.$currentFill.set(this.stateApi.getCurrentFill());
this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity());

View File

@ -11,7 +11,6 @@ import type {
CanvasEraserLineState,
CanvasInpaintMaskState,
CanvasRegionalGuidanceState,
CanvasV2State,
Coordinate,
Rect,
} from 'features/controlLayers/store/types';
@ -83,11 +82,7 @@ export class CanvasMaskAdapter extends CanvasModuleBase {
this.konva.layer.destroy();
};
update = async (arg?: {
state: CanvasMaskAdapter['state'];
toolState: CanvasV2State['tool'];
isSelected: boolean;
}) => {
update = async (arg?: { state: CanvasMaskAdapter['state'] }) => {
const state = get(arg, 'state', this.state);
if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) {

View File

@ -28,7 +28,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
}
render = async () => {
const state = this.manager.stateApi.getState();
const state = this.manager.stateApi.getCanvasState();
if (!this.state) {
this.log.trace('First render');
@ -51,7 +51,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
await this.renderStagingArea(state, prevState);
this.arrangeEntities(state, prevState);
this.manager.stateApi.$toolState.set(state.tool);
this.manager.stateApi.$toolState.set(this.manager.stateApi.getToolState());
this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier);
this.manager.stateApi.$selectedEntity.set(this.manager.stateApi.getSelectedEntity());
this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill());
@ -96,11 +96,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
adapterMap.set(adapter.id, adapter);
this.manager.stage.addLayer(adapter.konva.layer);
}
await adapter.update({
state: entityState,
toolState: state.tool,
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
});
await adapter.update({ state: entityState });
}
}
};
@ -129,11 +125,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
adapterMap.set(adapter.id, adapter);
this.manager.stage.addLayer(adapter.konva.layer);
}
await adapter.update({
state: entityState,
toolState: state.tool,
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
});
await adapter.update({ state: entityState });
}
}
};
@ -167,11 +159,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
adapterMap.set(adapter.id, adapter);
this.manager.stage.addLayer(adapter.konva.layer);
}
await adapter.update({
state: entityState,
toolState: state.tool,
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
});
await adapter.update({ state: entityState });
}
}
};
@ -205,11 +193,7 @@ export class CanvasRenderingModule extends CanvasModuleBase {
adapterMap.set(adapter.id, adapter);
this.manager.stage.addLayer(adapter.konva.layer);
}
await adapter.update({
state: entityState,
toolState: state.tool,
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
});
await adapter.update({ state: entityState });
}
}
};

View File

@ -7,7 +7,6 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
bboxChanged,
brushWidthChanged,
entityBrushLineAdded,
entityEraserLineAdded,
entityMoved,
@ -15,17 +14,20 @@ import {
entityRectAdded,
entityReset,
entitySelected,
eraserWidthChanged,
fillChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors';
import {
brushWidthChanged,
eraserWidthChanged,
fillChanged,
type ToolState,
} from 'features/controlLayers/store/toolSlice';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
CanvasV2State,
Coordinate,
EntityBrushLineAddedPayload,
EntityEraserLineAddedPayload,
@ -97,7 +99,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
}
// Reminder - use arrow functions to avoid binding issues
getState = () => {
getCanvasState = () => {
return this.store.getState().canvasV2;
};
resetEntity = (arg: EntityIdentifierPayload) => {
@ -141,36 +143,36 @@ export class CanvasStateApiModule extends CanvasModuleBase {
);
};
getBbox = () => {
return this.getState().bbox;
return this.getCanvasState().bbox;
};
getToolState = () => {
return this.getState().tool;
return this.store.getState().tool;
};
getSettings = () => {
return this.getState().settings;
return this.getCanvasState().settings;
};
getRegionsState = () => {
return this.getState().regions;
return this.getCanvasState().regions;
};
getRasterLayersState = () => {
return this.getState().rasterLayers;
return this.getCanvasState().rasterLayers;
};
getControlLayersState = () => {
return this.getState().controlLayers;
return this.getCanvasState().controlLayers;
};
getInpaintMasksState = () => {
return this.getState().inpaintMasks;
return this.getCanvasState().inpaintMasks;
};
getSession = () => {
return this.getState().session;
return this.getCanvasState().session;
};
getIsSelected = (id: string) => {
return this.getState().selectedEntityIdentifier?.id === id;
return this.getCanvasState().selectedEntityIdentifier?.id === id;
};
getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null {
const state = this.getState();
const state = this.getCanvasState();
let entityState: EntityStateAndAdapter['state'] | null = null;
let entityAdapter: EntityStateAndAdapter['adapter'] | null = null;
@ -202,7 +204,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
}
getRenderedEntityCount = () => {
const renderableEntities = selectAllRenderableEntities(this.getState());
const renderableEntities = selectAllRenderableEntities(this.getCanvasState());
let count = 0;
for (const entity of renderableEntities) {
if (entity.isEnabled) {
@ -213,7 +215,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
};
getSelectedEntity = () => {
const state = this.getState();
const state = this.getCanvasState();
if (state.selectedEntityIdentifier) {
return this.getEntity(state.selectedEntityIdentifier);
}
@ -221,8 +223,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
};
getCurrentFill = () => {
const state = this.getState();
let currentFill: RgbaColor = state.tool.fill;
let currentFill: RgbaColor = this.getToolState().fill;
const selectedEntity = this.getSelectedEntity();
if (selectedEntity) {
// These two entity types use a compositing rect for opacity. Their fill is always a solid color.
@ -239,15 +240,14 @@ export class CanvasStateApiModule extends CanvasModuleBase {
// The brush should use the mask opacity for these enktity types
return { ...selectedEntity.state.fill.color, a: 1 };
} else {
const state = this.getState();
return state.tool.fill;
return this.getToolState().fill;
}
};
$transformingEntity = atom<CanvasEntityIdentifier | null>(null);
$isProcessingTransform = atom<boolean>(false);
$toolState: WritableAtom<CanvasV2State['tool']> = atom();
$toolState: WritableAtom<ToolState> = atom();
$currentFill: WritableAtom<RgbaColor> = atom();
$selectedEntity: WritableAtom<EntityStateAndAdapter | null> = atom();
$selectedEntityIdentifier: WritableAtom<CanvasEntityIdentifier | null> = atom();

View File

@ -15,7 +15,6 @@ import { regionsReducers } from 'features/controlLayers/store/regionsReducers';
import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors';
import { sessionReducers } from 'features/controlLayers/store/sessionReducers';
import { settingsReducers } from 'features/controlLayers/store/settingsReducers';
import { toolReducers } from 'features/controlLayers/store/toolReducers';
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
@ -57,16 +56,6 @@ const initialState: CanvasV2State = {
},
loras: [],
ipAdapters: { entities: [] },
tool: {
invertScroll: false,
fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
brush: {
width: 50,
},
eraser: {
width: 50,
},
},
bbox: {
rect: { x: 0, y: 0, width: 512, height: 512 },
optimalDimension: 512,
@ -109,7 +98,6 @@ export const canvasV2Slice = createSlice({
// move out
...lorasReducers,
...settingsReducers,
...toolReducers,
...sessionReducers,
entitySelected: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
@ -364,7 +352,6 @@ export const canvasV2Slice = createSlice({
const size = pick(state.bbox.rect, 'width', 'height');
state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, state.bbox.optimalDimension);
state.session = deepClone(initialState.session);
state.tool = deepClone(initialState.tool);
state.ipAdapters = deepClone(initialState.ipAdapters);
state.rasterLayers = deepClone(initialState.rasterLayers);
@ -403,10 +390,6 @@ export const canvasV2Slice = createSlice({
});
export const {
brushWidthChanged,
eraserWidthChanged,
fillChanged,
invertScrollChanged,
clipToBboxChanged,
canvasReset,
settingsDynamicGridToggled,

View File

@ -1,17 +0,0 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import type { CanvasV2State, RgbaColor } from 'features/controlLayers/store/types';
export const toolReducers = {
brushWidthChanged: (state, action: PayloadAction<number>) => {
state.tool.brush.width = Math.round(action.payload);
},
eraserWidthChanged: (state, action: PayloadAction<number>) => {
state.tool.eraser.width = Math.round(action.payload);
},
fillChanged: (state, action: PayloadAction<RgbaColor>) => {
state.tool.fill = action.payload;
},
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
state.tool.invertScroll = action.payload;
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -0,0 +1,54 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig } from 'app/store/store';
import type { RgbaColor } from 'features/controlLayers/store/types';
export type ToolState = {
invertScroll: boolean;
brush: { width: number };
eraser: { width: number };
fill: RgbaColor;
};
const initialState: ToolState = {
invertScroll: false,
fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
brush: {
width: 50,
},
eraser: {
width: 50,
},
};
export const toolSlice = createSlice({
name: 'tool',
initialState,
reducers: {
brushWidthChanged: (state, action: PayloadAction<number>) => {
state.brush.width = Math.round(action.payload);
},
eraserWidthChanged: (state, action: PayloadAction<number>) => {
state.eraser.width = Math.round(action.payload);
},
fillChanged: (state, action: PayloadAction<RgbaColor>) => {
state.fill = action.payload;
},
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
state.invertScroll = action.payload;
},
},
});
export const { brushWidthChanged, eraserWidthChanged, fillChanged, invertScrollChanged } = toolSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const toolPersistConfig: PersistConfig<ToolState> = {
name: toolSlice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -715,12 +715,6 @@ export type CanvasV2State = {
entities: CanvasIPAdapterState[];
};
loras: LoRA[];
tool: {
invertScroll: boolean;
brush: { width: number };
eraser: { width: number };
fill: RgbaColor;
};
settings: {
imageSmoothing: boolean;
showHUD: boolean;