From 14d0bfbef66948d4c599c05b90ee6849bc3c2b45 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 21:16:36 +1000 Subject: [PATCH] refactor(ui): divvy up canvas state a bit --- .../src/common/hooks/useIsReadyToEnqueue.ts | 15 ++--- .../components/AddPromptButtons.tsx | 4 +- .../ControlAdapter/CAActionsMenu.tsx | 4 +- .../components/ControlLayersPanelContent.tsx | 10 ++-- .../components/DeleteAllLayersButton.tsx | 8 +-- .../components/Layer/LayerActionsMenu.tsx | 4 +- .../RegionalGuidance/RGActionsMenu.tsx | 4 +- .../controlLayers/konva/renderers/renderer.ts | 28 ++++----- .../controlLayers/store/canvasV2Slice.ts | 25 ++++---- .../store/controlAdaptersReducers.ts | 22 +++---- .../controlLayers/store/ipAdaptersReducers.ts | 10 ++-- .../controlLayers/store/layersReducers.ts | 58 ++++++++++--------- .../controlLayers/store/regionsReducers.ts | 20 +++---- .../features/controlLayers/store/selectors.ts | 5 +- .../src/features/controlLayers/store/types.ts | 12 ++-- .../deleteImageModal/store/selectors.ts | 8 +-- .../nodes/util/graph/generation/addLayers.ts | 6 +- .../generation/buildGenerationTabGraph.ts | 6 +- .../generation/buildGenerationTabSDXLGraph.ts | 6 +- .../ParametersPanelTextToImage.tsx | 3 +- 20 files changed, 128 insertions(+), 130 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index c10a9af006..eb9fc76014 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -124,7 +124,7 @@ const createSelector = (templates: Templates) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - canvasV2.controlAdapters + canvasV2.controlAdapters.entities .filter((ca) => ca.isEnabled) .forEach((ca, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -160,7 +160,7 @@ const createSelector = (templates: Templates) => } }); - canvasV2.ipAdapters + canvasV2.ipAdapters.entities .filter((ipa) => ipa.isEnabled) .forEach((ipa, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -188,7 +188,7 @@ const createSelector = (templates: Templates) => } }); - canvasV2.regions + canvasV2.regions.entities .filter((rg) => rg.isEnabled) .forEach((rg, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -225,7 +225,7 @@ const createSelector = (templates: Templates) => } }); - canvasV2.layers + canvasV2.layers.entities .filter((l) => l.isEnabled) .forEach((l, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -234,13 +234,6 @@ const createSelector = (templates: Templates) => const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; - // if (l.type === 'initial_image_layer') { - // // Must have an image - // if (!l.image) { - // problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); - // } - // } - if (problems.length) { const content = upperFirst(problems.join(', ')); reasons.push({ prefix, content }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 3f4222b202..7256d6d9e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -21,8 +21,8 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (caState) => { - const rg = caState.regions.find((rg) => rg.id === id); + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const rg = canvasV2.regions.entities.find((rg) => rg.id === id); return { canAddPositivePrompt: rg?.positivePrompt === null, canAddNegativePrompt: rg?.negativePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx index 2bf7537377..1c532dda0f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -32,8 +32,8 @@ export const CAActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const ca = selectCAOrThrow(canvasV2, id); - const caIndex = canvasV2.controlAdapters.indexOf(ca); - const caCount = canvasV2.controlAdapters.length; + const caIndex = canvasV2.controlAdapters.entities.indexOf(ca); + const caCount = canvasV2.controlAdapters.entities.length; return { canMoveForward: caIndex < caCount - 1, canMoveBackward: caIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index c462048335..405bf43c21 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -15,11 +15,11 @@ import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice' import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2State) => { - const rgIds = canvasV2State.regions.map(mapId).reverse(); - const caIds = canvasV2State.controlAdapters.map(mapId).reverse(); - const ipaIds = canvasV2State.ipAdapters.map(mapId).reverse(); - const layerIds = canvasV2State.layers.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const rgIds = canvasV2.regions.entities.map(mapId).reverse(); + const caIds = canvasV2.controlAdapters.entities.map(mapId).reverse(); + const ipaIds = canvasV2.ipAdapters.entities.map(mapId).reverse(); + const layerIds = canvasV2.layers.entities.map(mapId).reverse(); const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length; return { rgIds, caIds, ipaIds, layerIds, entityCount }; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index fbe9c22f68..16a3c88d83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -10,10 +10,10 @@ export const DeleteAllLayersButton = memo(() => { const dispatch = useAppDispatch(); const entityCount = useAppSelector((s) => { return ( - s.canvasV2.regions.length + - s.canvasV2.controlAdapters.length + - s.canvasV2.ipAdapters.length + - s.canvasV2.layers.length + s.canvasV2.regions.entities.length + + s.canvasV2.controlAdapters.entities.length + + s.canvasV2.ipAdapters.entities.length + + s.canvasV2.layers.entities.length ); }); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx index 6c161531dd..7ab753012f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -32,8 +32,8 @@ export const LayerActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const layer = selectLayerOrThrow(canvasV2, id); - const layerIndex = canvasV2.layers.indexOf(layer); - const layerCount = canvasV2.layers.length; + const layerIndex = canvasV2.layers.entities.indexOf(layer); + const layerCount = canvasV2.layers.entities.length; return { canMoveForward: layerIndex < layerCount - 1, canMoveBackward: layerIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx index 1df3467959..784253b5d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -39,8 +39,8 @@ export const RGActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const rg = selectRGOrThrow(canvasV2, id); - const rgIndex = canvasV2.regions.indexOf(rg); - const rgCount = canvasV2.regions.length; + const rgIndex = canvasV2.regions.entities.indexOf(rg); + const rgCount = canvasV2.regions.entities.length; return { isMoveForwardOneDisabled: rgIndex < rgCount - 1, isMoveBackardOneDisabled: rgIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index dcf283984d..3c888053a2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -170,13 +170,13 @@ export const initializeRenderer = ( if (!identifier) { selectedEntity = null; } else if (identifier.type === 'layer') { - selectedEntity = canvasV2.layers.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.layers.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'control_adapter') { - selectedEntity = canvasV2.controlAdapters.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'ip_adapter') { - selectedEntity = canvasV2.ipAdapters.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { - selectedEntity = canvasV2.regions.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.regions.entities.find((i) => i.id === identifier.id) ?? null; } else { selectedEntity = null; } @@ -304,23 +304,23 @@ export const initializeRenderer = ( if ( isFirstRender || - canvasV2.layers !== prevCanvasV2.layers || + canvasV2.layers.entities !== prevCanvasV2.layers.entities || canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - renderLayers(manager, canvasV2.layers, canvasV2.tool.selected, onPosChanged); + renderLayers(manager, canvasV2.layers.entities, canvasV2.tool.selected, onPosChanged); } if ( isFirstRender || - canvasV2.regions !== prevCanvasV2.regions || + canvasV2.regions.entities !== prevCanvasV2.regions.entities || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering regions'); renderRegions( manager, - canvasV2.regions, + canvasV2.regions.entities, canvasV2.settings.maskOpacity, canvasV2.tool.selected, canvasV2.selectedEntityIdentifier, @@ -328,9 +328,9 @@ export const initializeRenderer = ( ); } - if (isFirstRender || canvasV2.controlAdapters !== prevCanvasV2.controlAdapters) { + if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { logIfDebugging('Rendering control adapters'); - renderControlAdapters(manager, canvasV2.controlAdapters); + renderControlAdapters(manager, canvasV2.controlAdapters.entities); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { @@ -355,12 +355,12 @@ export const initializeRenderer = ( if ( isFirstRender || - canvasV2.layers !== prevCanvasV2.layers || - canvasV2.controlAdapters !== prevCanvasV2.controlAdapters || - canvasV2.regions !== prevCanvasV2.regions + canvasV2.layers.entities !== prevCanvasV2.layers.entities || + canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || + canvasV2.regions.entities !== prevCanvasV2.regions.entities ) { logIfDebugging('Arranging entities'); - arrangeEntities(manager, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions); + arrangeEntities(manager, canvasV2.layers.entities, canvasV2.controlAdapters.entities, canvasV2.regions.entities); } prevCanvasV2 = canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 3de6e777de..9d9088f75c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -16,18 +16,17 @@ 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 { atom } from 'nanostores'; -import type { ImageDTO } from 'services/api/types'; import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; -import { DEFAULT_RGBA_COLOR, imageDTOToImageWithDims } from './types'; +import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, - layers: [], - controlAdapters: [], - ipAdapters: [], - regions: [], + layers: { entities: [], baseLayerImageCache: null }, + controlAdapters: { entities: [] }, + ipAdapters: { entities: [] }, + regions: { entities: [] }, loras: [], inpaintMask: { bbox: null, @@ -120,7 +119,6 @@ const initialState: CanvasV2State = { refinerNegativeAestheticScore: 2.5, refinerStart: 0.8, }, - baseLayerImageCache: null, }; export const canvasV2Slice = createSlice({ @@ -162,14 +160,11 @@ export const canvasV2Slice = createSlice({ state.selectedEntityIdentifier = action.payload; }, allEntitiesDeleted: (state) => { - state.regions = []; - state.layers = []; - state.ipAdapters = []; - state.controlAdapters = []; - state.baseLayerImageCache = null; - }, - baseLayerImageCacheChanged: (state, action: PayloadAction) => { - state.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null; + state.regions.entities = []; + state.layers.entities = []; + state.layers.baseLayerImageCache = null; + state.ipAdapters.entities = []; + state.controlAdapters.entities = []; }, }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index c12c93a0d0..0601061f73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -20,7 +20,7 @@ import type { } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; -export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); +export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.entities.find((ca) => ca.id === id); export const selectCAOrThrow = (state: CanvasV2State, id: string) => { const ca = selectCA(state, id); assert(ca, `Control Adapter with id ${id} not found`); @@ -31,7 +31,7 @@ export const controlAdaptersReducers = { caAdded: { reducer: (state, action: PayloadAction<{ id: string; config: ControlNetConfig | T2IAdapterConfig }>) => { const { id, config } = action.payload; - state.controlAdapters.push({ + state.controlAdapters.entities.push({ id, type: 'control_adapter', x: 0, @@ -52,7 +52,7 @@ export const controlAdaptersReducers = { }, caRecalled: (state, action: PayloadAction<{ data: ControlAdapterEntity }>) => { const { data } = action.payload; - state.controlAdapters.push(data); + state.controlAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; }, caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { @@ -83,10 +83,10 @@ export const controlAdaptersReducers = { }, caDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id); + state.controlAdapters.entities = state.controlAdapters.entities.filter((ca) => ca.id !== id); }, caAllDeleted: (state) => { - state.controlAdapters = []; + state.controlAdapters.entities = []; }, caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; @@ -102,7 +102,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveOneToEnd(state.controlAdapters, ca); + moveOneToEnd(state.controlAdapters.entities, ca); }, caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -110,7 +110,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveToEnd(state.controlAdapters, ca); + moveToEnd(state.controlAdapters.entities, ca); }, caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -118,7 +118,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveOneToStart(state.controlAdapters, ca); + moveOneToStart(state.controlAdapters.entities, ca); }, caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -126,7 +126,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveToStart(state.controlAdapters, ca); + moveToStart(state.controlAdapters.entities, ca); }, caImageChanged: { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { @@ -195,11 +195,11 @@ export const controlAdaptersReducers = { // We may need to convert the CA to match the model if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') { const convertedCA: ControlNetData = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' }; - state.controlAdapters.splice(state.controlAdapters.indexOf(ca), 1, convertedCA); + state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); } else if (ca.adapterType === 'controlnet' && ca.model.type === 't2i_adapter') { const { controlMode: _, ...rest } = ca; const convertedCA: T2IAdapterData = { ...rest, adapterType: 't2i_adapter' }; - state.controlAdapters.splice(state.controlAdapters.indexOf(ca), 1, convertedCA); + state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); } }, caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index bf9894d6c7..ce29909b7d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -13,7 +13,7 @@ import type { } from './types'; import { imageDTOToImageObject } from './types'; -export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); +export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); export const selectIPAOrThrow = (state: CanvasV2State, id: string) => { const ipa = selectIPA(state, id); assert(ipa, `IP Adapter with id ${id} not found`); @@ -30,14 +30,14 @@ export const ipAdaptersReducers = { isEnabled: true, ...config, }; - state.ipAdapters.push(layer); + state.ipAdapters.entities.push(layer); state.selectedEntityIdentifier = { type: 'ip_adapter', id }; }, prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), }, ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterEntity }>) => { const { data } = action.payload; - state.ipAdapters.push(data); + state.ipAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id }; }, ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { @@ -49,10 +49,10 @@ export const ipAdaptersReducers = { }, ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== id); + state.ipAdapters.entities = state.ipAdapters.entities.filter((ipa) => ipa.id !== id); }, ipaAllDeleted: (state) => { - state.ipAdapters = []; + state.ipAdapters.entities = []; }, ipaImageChanged: { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 1f4dbeedc1..c7303f00cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -2,6 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { IRect } from 'konva/lib/types'; +import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -14,9 +15,9 @@ import type { PointAddedToLineArg, RectShapeAddedArg, } from './types'; -import { imageDTOToImageObject, isLine } from './types'; +import { imageDTOToImageObject, imageDTOToImageWithDims, isLine } from './types'; -export const selectLayer = (state: CanvasV2State, id: string) => state.layers.find((layer) => layer.id === id); +export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { const layer = selectLayer(state, id); assert(layer, `Layer with id ${id} not found`); @@ -27,7 +28,7 @@ export const layersReducers = { layerAdded: { reducer: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.layers.push({ + state.layers.entities.push({ id, type: 'layer', isEnabled: true, @@ -39,15 +40,15 @@ export const layersReducers = { y: 0, }); state.selectedEntityIdentifier = { type: 'layer', id }; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: () => ({ payload: { id: uuidv4() } }), }, layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { const { data } = action.payload; - state.layers.push(data); + state.layers.entities.push(data); state.selectedEntityIdentifier = { type: 'layer', id: data.id }; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -56,7 +57,7 @@ export const layersReducers = { return; } layer.isEnabled = !layer.isEnabled; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; @@ -66,7 +67,7 @@ export const layersReducers = { } layer.x = x; layer.y = y; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; @@ -92,16 +93,16 @@ export const layersReducers = { layer.objects = []; layer.bbox = null; layer.bboxNeedsUpdate = false; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.layers = state.layers.filter((l) => l.id !== id); - state.baseLayerImageCache = null; + state.layers.entities = state.layers.entities.filter((l) => l.id !== id); + state.layers.baseLayerImageCache = null; }, layerAllDeleted: (state) => { - state.layers = []; - state.baseLayerImageCache = null; + state.layers.entities = []; + state.layers.baseLayerImageCache = null; }, layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; @@ -110,7 +111,7 @@ export const layersReducers = { return; } layer.opacity = opacity; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -118,8 +119,8 @@ export const layersReducers = { if (!layer) { return; } - moveOneToEnd(state.layers, layer); - state.baseLayerImageCache = null; + moveOneToEnd(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -127,8 +128,8 @@ export const layersReducers = { if (!layer) { return; } - moveToEnd(state.layers, layer); - state.baseLayerImageCache = null; + moveToEnd(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -136,8 +137,8 @@ export const layersReducers = { if (!layer) { return; } - moveOneToStart(state.layers, layer); - state.baseLayerImageCache = null; + moveOneToStart(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -145,8 +146,8 @@ export const layersReducers = { if (!layer) { return; } - moveToStart(state.layers, layer); - state.baseLayerImageCache = null; + moveToStart(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { @@ -165,7 +166,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -187,7 +188,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -205,7 +206,7 @@ export const layersReducers = { } lastObject.points.push(...point); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerRectAdded: { reducer: (state, action: PayloadAction) => { @@ -225,7 +226,7 @@ export const layersReducers = { color, }); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, @@ -238,8 +239,11 @@ export const layersReducers = { } layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO)); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }), }, + baseLayerImageCacheChanged: (state, action: PayloadAction) => { + state.layers.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null; + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index ff64111d8b..752e6a0af1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -26,7 +26,7 @@ import type { } from './types'; import { isLine } from './types'; -export const selectRG = (state: CanvasV2State, id: string) => state.regions.find((rg) => rg.id === id); +export const selectRG = (state: CanvasV2State, id: string) => state.regions.entities.find((rg) => rg.id === id); export const selectRGOrThrow = (state: CanvasV2State, id: string) => { const rg = selectRG(state, id); assert(rg, `Region with id ${id} not found`); @@ -44,7 +44,7 @@ const DEFAULT_MASK_COLORS: RgbColor[] = [ ]; const getRGMaskFill = (state: CanvasV2State): RgbColor => { - const lastFill = state.regions.slice(-1)[0]?.fill; + const lastFill = state.regions.entities.slice(-1)[0]?.fill; let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); if (i === -1) { i = 0; @@ -75,7 +75,7 @@ export const regionsReducers = { ipAdapters: [], imageCache: null, }; - state.regions.push(rg); + state.regions.entities.push(rg); state.selectedEntityIdentifier = { type: 'regional_guidance', id }; }, prepare: () => ({ payload: { id: uuidv4() } }), @@ -93,7 +93,7 @@ export const regionsReducers = { }, rgRecalled: (state, action: PayloadAction<{ data: RegionEntity }>) => { const { data } = action.payload; - state.regions.push(data); + state.regions.entities.push(data); state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { @@ -121,10 +121,10 @@ export const regionsReducers = { }, rgDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.regions = state.regions.filter((ca) => ca.id !== id); + state.regions.entities = state.regions.entities.filter((ca) => ca.id !== id); }, rgAllDeleted: (state) => { - state.regions = []; + state.regions.entities = []; }, rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -132,7 +132,7 @@ export const regionsReducers = { if (!rg) { return; } - moveOneToEnd(state.regions, rg); + moveOneToEnd(state.regions.entities, rg); }, rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -140,7 +140,7 @@ export const regionsReducers = { if (!rg) { return; } - moveToEnd(state.regions, rg); + moveToEnd(state.regions.entities, rg); }, rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -148,7 +148,7 @@ export const regionsReducers = { if (!rg) { return; } - moveOneToStart(state.regions, rg); + moveOneToStart(state.regions.entities, rg); }, rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -156,7 +156,7 @@ export const regionsReducers = { if (!rg) { return; } - moveToStart(state.regions, rg); + moveToStart(state.regions.entities, rg); }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index ebd59de480..4a3fd7812d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -4,7 +4,10 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { return ( - canvasV2.regions.length + canvasV2.controlAdapters.length + canvasV2.ipAdapters.length + canvasV2.layers.length + canvasV2.regions.entities.length + + canvasV2.controlAdapters.entities.length + + canvasV2.ipAdapters.entities.length + + canvasV2.layers.entities.length ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f39f097920..44ea8cb500 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -796,10 +796,13 @@ export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMask: InpaintMaskEntity; - layers: LayerEntity[]; - controlAdapters: ControlAdapterEntity[]; - ipAdapters: IPAdapterEntity[]; - regions: RegionEntity[]; + layers: { + baseLayerImageCache: ImageWithDims | null; + entities: LayerEntity[]; + }; + controlAdapters: { entities: ControlAdapterEntity[] }; + ipAdapters: { entities: IPAdapterEntity[] }; + regions: { entities: RegionEntity[] }; loras: LoRA[]; tool: { selected: Tool; @@ -872,7 +875,6 @@ export type CanvasV2State = { refinerNegativeAestheticScore: number; refinerStart: number; }; - baseLayerImageCache: ImageWithDims | null; }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 21b5177e21..7207ea80d4 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -11,7 +11,7 @@ import { some } from 'lodash-es'; import type { ImageUsage } from './types'; export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { - const isLayerImage = canvasV2.layers.some((layer) => + const isLayerImage = canvasV2.layers.entities.some((layer) => layer.objects.some((obj) => obj.type === 'image' && obj.image.name === image_name) ); @@ -21,11 +21,11 @@ export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_ some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name) ); - const isControlAdapterImage = canvasV2.controlAdapters.some( - (ca) => ca.image?.name === image_name || ca.processedImage?.name === image_name + const isControlAdapterImage = canvasV2.controlAdapters.entities.some( + (ca) => ca.imageObject?.image.name === image_name || ca.processedImageObject?.image.name === image_name ); - const isIPAdapterImage = canvasV2.ipAdapters.some((ipa) => ipa.imageObject?.name === image_name); + const isIPAdapterImage = canvasV2.ipAdapters.entities.some((ipa) => ipa.imageObject?.image.name === image_name); const imageUsage: ImageUsage = { isLayerImage, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index 8d4b74e76c..d665d53d25 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -78,13 +78,13 @@ const getBaseLayer = async (layers: LayerEntity[], bbox: IRect, preview: boolean export const getBaseLayerImage = async (): Promise => { const { dispatch, getState } = getStore(); const state = getState(); - if (state.canvasV2.baseLayerImageCache) { - const imageDTO = await getImageDTO(state.canvasV2.baseLayerImageCache.name); + if (state.canvasV2.layers.baseLayerImageCache) { + const imageDTO = await getImageDTO(state.canvasV2.layers.baseLayerImageCache.name); if (imageDTO) { return imageDTO; } } - const blob = await getBaseLayer(state.canvasV2.layers, state.canvasV2.bbox, true); + const blob = await getBaseLayer(state.canvasV2.layers.entities, state.canvasV2.bbox, true); const file = new File([blob], 'image.png', { type: 'image/png' }); const req = dispatch( imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'general', is_intermediate: true }) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index acd502c409..d9adb21cd5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -156,10 +156,10 @@ export const buildGenerationTabGraph = async (state: RootState): Promise { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); + const controlLayersCount = useAppSelector(selectEntityCount); const controlLayersTitle = useMemo(() => { if (controlLayersCount === 0) { return t('controlLayers.controlLayers');