refactor(ui): divvy up canvas state a bit

This commit is contained in:
psychedelicious 2024-06-20 21:16:36 +10:00
parent 0c9cf73702
commit 14d0bfbef6
20 changed files with 128 additions and 130 deletions

View File

@ -124,7 +124,7 @@ const createSelector = (templates: Templates) =>
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
} }
canvasV2.controlAdapters canvasV2.controlAdapters.entities
.filter((ca) => ca.isEnabled) .filter((ca) => ca.isEnabled)
.forEach((ca, i) => { .forEach((ca, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one'); const layerLiteral = i18n.t('controlLayers.layers_one');
@ -160,7 +160,7 @@ const createSelector = (templates: Templates) =>
} }
}); });
canvasV2.ipAdapters canvasV2.ipAdapters.entities
.filter((ipa) => ipa.isEnabled) .filter((ipa) => ipa.isEnabled)
.forEach((ipa, i) => { .forEach((ipa, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one'); const layerLiteral = i18n.t('controlLayers.layers_one');
@ -188,7 +188,7 @@ const createSelector = (templates: Templates) =>
} }
}); });
canvasV2.regions canvasV2.regions.entities
.filter((rg) => rg.isEnabled) .filter((rg) => rg.isEnabled)
.forEach((rg, i) => { .forEach((rg, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one'); const layerLiteral = i18n.t('controlLayers.layers_one');
@ -225,7 +225,7 @@ const createSelector = (templates: Templates) =>
} }
}); });
canvasV2.layers canvasV2.layers.entities
.filter((l) => l.isEnabled) .filter((l) => l.isEnabled)
.forEach((l, i) => { .forEach((l, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one'); const layerLiteral = i18n.t('controlLayers.layers_one');
@ -234,13 +234,6 @@ const createSelector = (templates: Templates) =>
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = []; 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) { if (problems.length) {
const content = upperFirst(problems.join(', ')); const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content }); reasons.push({ prefix, content });

View File

@ -21,8 +21,8 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => {
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id);
const selectValidActions = useMemo( const selectValidActions = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (caState) => { createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rg = caState.regions.find((rg) => rg.id === id); const rg = canvasV2.regions.entities.find((rg) => rg.id === id);
return { return {
canAddPositivePrompt: rg?.positivePrompt === null, canAddPositivePrompt: rg?.positivePrompt === null,
canAddNegativePrompt: rg?.negativePrompt === null, canAddNegativePrompt: rg?.negativePrompt === null,

View File

@ -32,8 +32,8 @@ export const CAActionsMenu = memo(({ id }: Props) => {
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const ca = selectCAOrThrow(canvasV2, id); const ca = selectCAOrThrow(canvasV2, id);
const caIndex = canvasV2.controlAdapters.indexOf(ca); const caIndex = canvasV2.controlAdapters.entities.indexOf(ca);
const caCount = canvasV2.controlAdapters.length; const caCount = canvasV2.controlAdapters.entities.length;
return { return {
canMoveForward: caIndex < caCount - 1, canMoveForward: caIndex < caCount - 1,
canMoveBackward: caIndex > 0, canMoveBackward: caIndex > 0,

View File

@ -15,11 +15,11 @@ import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2State) => { const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rgIds = canvasV2State.regions.map(mapId).reverse(); const rgIds = canvasV2.regions.entities.map(mapId).reverse();
const caIds = canvasV2State.controlAdapters.map(mapId).reverse(); const caIds = canvasV2.controlAdapters.entities.map(mapId).reverse();
const ipaIds = canvasV2State.ipAdapters.map(mapId).reverse(); const ipaIds = canvasV2.ipAdapters.entities.map(mapId).reverse();
const layerIds = canvasV2State.layers.map(mapId).reverse(); const layerIds = canvasV2.layers.entities.map(mapId).reverse();
const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length; const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length;
return { rgIds, caIds, ipaIds, layerIds, entityCount }; return { rgIds, caIds, ipaIds, layerIds, entityCount };
}); });

View File

@ -10,10 +10,10 @@ export const DeleteAllLayersButton = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const entityCount = useAppSelector((s) => { const entityCount = useAppSelector((s) => {
return ( return (
s.canvasV2.regions.length + s.canvasV2.regions.entities.length +
s.canvasV2.controlAdapters.length + s.canvasV2.controlAdapters.entities.length +
s.canvasV2.ipAdapters.length + s.canvasV2.ipAdapters.entities.length +
s.canvasV2.layers.length s.canvasV2.layers.entities.length
); );
}); });
const onClick = useCallback(() => { const onClick = useCallback(() => {

View File

@ -32,8 +32,8 @@ export const LayerActionsMenu = memo(({ id }: Props) => {
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const layer = selectLayerOrThrow(canvasV2, id); const layer = selectLayerOrThrow(canvasV2, id);
const layerIndex = canvasV2.layers.indexOf(layer); const layerIndex = canvasV2.layers.entities.indexOf(layer);
const layerCount = canvasV2.layers.length; const layerCount = canvasV2.layers.entities.length;
return { return {
canMoveForward: layerIndex < layerCount - 1, canMoveForward: layerIndex < layerCount - 1,
canMoveBackward: layerIndex > 0, canMoveBackward: layerIndex > 0,

View File

@ -39,8 +39,8 @@ export const RGActionsMenu = memo(({ id }: Props) => {
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rg = selectRGOrThrow(canvasV2, id); const rg = selectRGOrThrow(canvasV2, id);
const rgIndex = canvasV2.regions.indexOf(rg); const rgIndex = canvasV2.regions.entities.indexOf(rg);
const rgCount = canvasV2.regions.length; const rgCount = canvasV2.regions.entities.length;
return { return {
isMoveForwardOneDisabled: rgIndex < rgCount - 1, isMoveForwardOneDisabled: rgIndex < rgCount - 1,
isMoveBackardOneDisabled: rgIndex > 0, isMoveBackardOneDisabled: rgIndex > 0,

View File

@ -170,13 +170,13 @@ export const initializeRenderer = (
if (!identifier) { if (!identifier) {
selectedEntity = null; selectedEntity = null;
} else if (identifier.type === 'layer') { } 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') { } 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') { } 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') { } 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 { } else {
selectedEntity = null; selectedEntity = null;
} }
@ -304,23 +304,23 @@ export const initializeRenderer = (
if ( if (
isFirstRender || isFirstRender ||
canvasV2.layers !== prevCanvasV2.layers || canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering layers'); logIfDebugging('Rendering layers');
renderLayers(manager, canvasV2.layers, canvasV2.tool.selected, onPosChanged); renderLayers(manager, canvasV2.layers.entities, canvasV2.tool.selected, onPosChanged);
} }
if ( if (
isFirstRender || isFirstRender ||
canvasV2.regions !== prevCanvasV2.regions || canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity ||
canvasV2.tool.selected !== prevCanvasV2.tool.selected canvasV2.tool.selected !== prevCanvasV2.tool.selected
) { ) {
logIfDebugging('Rendering regions'); logIfDebugging('Rendering regions');
renderRegions( renderRegions(
manager, manager,
canvasV2.regions, canvasV2.regions.entities,
canvasV2.settings.maskOpacity, canvasV2.settings.maskOpacity,
canvasV2.tool.selected, canvasV2.tool.selected,
canvasV2.selectedEntityIdentifier, 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'); logIfDebugging('Rendering control adapters');
renderControlAdapters(manager, canvasV2.controlAdapters); renderControlAdapters(manager, canvasV2.controlAdapters.entities);
} }
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
@ -355,12 +355,12 @@ export const initializeRenderer = (
if ( if (
isFirstRender || isFirstRender ||
canvasV2.layers !== prevCanvasV2.layers || canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
canvasV2.controlAdapters !== prevCanvasV2.controlAdapters || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
canvasV2.regions !== prevCanvasV2.regions canvasV2.regions.entities !== prevCanvasV2.regions.entities
) { ) {
logIfDebugging('Arranging 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; prevCanvasV2 = canvasV2;

View File

@ -16,18 +16,17 @@ import { toolReducers } from 'features/controlLayers/store/toolReducers';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import type { ImageDTO } from 'services/api/types';
import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types';
import { DEFAULT_RGBA_COLOR, imageDTOToImageWithDims } from './types'; import { DEFAULT_RGBA_COLOR } from './types';
const initialState: CanvasV2State = { const initialState: CanvasV2State = {
_version: 3, _version: 3,
selectedEntityIdentifier: null, selectedEntityIdentifier: null,
layers: [], layers: { entities: [], baseLayerImageCache: null },
controlAdapters: [], controlAdapters: { entities: [] },
ipAdapters: [], ipAdapters: { entities: [] },
regions: [], regions: { entities: [] },
loras: [], loras: [],
inpaintMask: { inpaintMask: {
bbox: null, bbox: null,
@ -120,7 +119,6 @@ const initialState: CanvasV2State = {
refinerNegativeAestheticScore: 2.5, refinerNegativeAestheticScore: 2.5,
refinerStart: 0.8, refinerStart: 0.8,
}, },
baseLayerImageCache: null,
}; };
export const canvasV2Slice = createSlice({ export const canvasV2Slice = createSlice({
@ -162,14 +160,11 @@ export const canvasV2Slice = createSlice({
state.selectedEntityIdentifier = action.payload; state.selectedEntityIdentifier = action.payload;
}, },
allEntitiesDeleted: (state) => { allEntitiesDeleted: (state) => {
state.regions = []; state.regions.entities = [];
state.layers = []; state.layers.entities = [];
state.ipAdapters = []; state.layers.baseLayerImageCache = null;
state.controlAdapters = []; state.ipAdapters.entities = [];
state.baseLayerImageCache = null; state.controlAdapters.entities = [];
},
baseLayerImageCacheChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null;
}, },
}, },
}); });

View File

@ -20,7 +20,7 @@ import type {
} from './types'; } from './types';
import { buildControlAdapterProcessorV2, imageDTOToImageObject } 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) => { export const selectCAOrThrow = (state: CanvasV2State, id: string) => {
const ca = selectCA(state, id); const ca = selectCA(state, id);
assert(ca, `Control Adapter with id ${id} not found`); assert(ca, `Control Adapter with id ${id} not found`);
@ -31,7 +31,7 @@ export const controlAdaptersReducers = {
caAdded: { caAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: ControlNetConfig | T2IAdapterConfig }>) => { reducer: (state, action: PayloadAction<{ id: string; config: ControlNetConfig | T2IAdapterConfig }>) => {
const { id, config } = action.payload; const { id, config } = action.payload;
state.controlAdapters.push({ state.controlAdapters.entities.push({
id, id,
type: 'control_adapter', type: 'control_adapter',
x: 0, x: 0,
@ -52,7 +52,7 @@ export const controlAdaptersReducers = {
}, },
caRecalled: (state, action: PayloadAction<{ data: ControlAdapterEntity }>) => { caRecalled: (state, action: PayloadAction<{ data: ControlAdapterEntity }>) => {
const { data } = action.payload; const { data } = action.payload;
state.controlAdapters.push(data); state.controlAdapters.entities.push(data);
state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id };
}, },
caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
@ -83,10 +83,10 @@ export const controlAdaptersReducers = {
}, },
caDeleted: (state, action: PayloadAction<{ id: string }>) => { caDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; 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) => { caAllDeleted: (state) => {
state.controlAdapters = []; state.controlAdapters.entities = [];
}, },
caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload; const { id, opacity } = action.payload;
@ -102,7 +102,7 @@ export const controlAdaptersReducers = {
if (!ca) { if (!ca) {
return; return;
} }
moveOneToEnd(state.controlAdapters, ca); moveOneToEnd(state.controlAdapters.entities, ca);
}, },
caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { caMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -110,7 +110,7 @@ export const controlAdaptersReducers = {
if (!ca) { if (!ca) {
return; return;
} }
moveToEnd(state.controlAdapters, ca); moveToEnd(state.controlAdapters.entities, ca);
}, },
caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -118,7 +118,7 @@ export const controlAdaptersReducers = {
if (!ca) { if (!ca) {
return; return;
} }
moveOneToStart(state.controlAdapters, ca); moveOneToStart(state.controlAdapters.entities, ca);
}, },
caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { caMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -126,7 +126,7 @@ export const controlAdaptersReducers = {
if (!ca) { if (!ca) {
return; return;
} }
moveToStart(state.controlAdapters, ca); moveToStart(state.controlAdapters.entities, ca);
}, },
caImageChanged: { caImageChanged: {
reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { 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 // We may need to convert the CA to match the model
if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') { if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') {
const convertedCA: ControlNetData = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' }; 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') { } else if (ca.adapterType === 'controlnet' && ca.model.type === 't2i_adapter') {
const { controlMode: _, ...rest } = ca; const { controlMode: _, ...rest } = ca;
const convertedCA: T2IAdapterData = { ...rest, adapterType: 't2i_adapter' }; 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 }>) => { caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {

View File

@ -13,7 +13,7 @@ import type {
} from './types'; } from './types';
import { imageDTOToImageObject } 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) => { export const selectIPAOrThrow = (state: CanvasV2State, id: string) => {
const ipa = selectIPA(state, id); const ipa = selectIPA(state, id);
assert(ipa, `IP Adapter with id ${id} not found`); assert(ipa, `IP Adapter with id ${id} not found`);
@ -30,14 +30,14 @@ export const ipAdaptersReducers = {
isEnabled: true, isEnabled: true,
...config, ...config,
}; };
state.ipAdapters.push(layer); state.ipAdapters.entities.push(layer);
state.selectedEntityIdentifier = { type: 'ip_adapter', id }; state.selectedEntityIdentifier = { type: 'ip_adapter', id };
}, },
prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }),
}, },
ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterEntity }>) => { ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterEntity }>) => {
const { data } = action.payload; const { data } = action.payload;
state.ipAdapters.push(data); state.ipAdapters.entities.push(data);
state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id }; state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id };
}, },
ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
@ -49,10 +49,10 @@ export const ipAdaptersReducers = {
}, },
ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { ipaDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; 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) => { ipaAllDeleted: (state) => {
state.ipAdapters = []; state.ipAdapters.entities = [];
}, },
ipaImageChanged: { ipaImageChanged: {
reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => {

View File

@ -2,6 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -14,9 +15,9 @@ import type {
PointAddedToLineArg, PointAddedToLineArg,
RectShapeAddedArg, RectShapeAddedArg,
} from './types'; } 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) => { export const selectLayerOrThrow = (state: CanvasV2State, id: string) => {
const layer = selectLayer(state, id); const layer = selectLayer(state, id);
assert(layer, `Layer with id ${id} not found`); assert(layer, `Layer with id ${id} not found`);
@ -27,7 +28,7 @@ export const layersReducers = {
layerAdded: { layerAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => { reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
state.layers.push({ state.layers.entities.push({
id, id,
type: 'layer', type: 'layer',
isEnabled: true, isEnabled: true,
@ -39,15 +40,15 @@ export const layersReducers = {
y: 0, y: 0,
}); });
state.selectedEntityIdentifier = { type: 'layer', id }; state.selectedEntityIdentifier = { type: 'layer', id };
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
prepare: () => ({ payload: { id: uuidv4() } }), prepare: () => ({ payload: { id: uuidv4() } }),
}, },
layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => {
const { data } = action.payload; const { data } = action.payload;
state.layers.push(data); state.layers.entities.push(data);
state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.selectedEntityIdentifier = { type: 'layer', id: data.id };
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -56,7 +57,7 @@ export const layersReducers = {
return; return;
} }
layer.isEnabled = !layer.isEnabled; layer.isEnabled = !layer.isEnabled;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload; const { id, x, y } = action.payload;
@ -66,7 +67,7 @@ export const layersReducers = {
} }
layer.x = x; layer.x = x;
layer.y = y; layer.y = y;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload; const { id, bbox } = action.payload;
@ -92,16 +93,16 @@ export const layersReducers = {
layer.objects = []; layer.objects = [];
layer.bbox = null; layer.bbox = null;
layer.bboxNeedsUpdate = false; layer.bboxNeedsUpdate = false;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerDeleted: (state, action: PayloadAction<{ id: string }>) => { layerDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
state.layers = state.layers.filter((l) => l.id !== id); state.layers.entities = state.layers.entities.filter((l) => l.id !== id);
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerAllDeleted: (state) => { layerAllDeleted: (state) => {
state.layers = []; state.layers.entities = [];
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload; const { id, opacity } = action.payload;
@ -110,7 +111,7 @@ export const layersReducers = {
return; return;
} }
layer.opacity = opacity; layer.opacity = opacity;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -118,8 +119,8 @@ export const layersReducers = {
if (!layer) { if (!layer) {
return; return;
} }
moveOneToEnd(state.layers, layer); moveOneToEnd(state.layers.entities, layer);
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -127,8 +128,8 @@ export const layersReducers = {
if (!layer) { if (!layer) {
return; return;
} }
moveToEnd(state.layers, layer); moveToEnd(state.layers.entities, layer);
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -136,8 +137,8 @@ export const layersReducers = {
if (!layer) { if (!layer) {
return; return;
} }
moveOneToStart(state.layers, layer); moveOneToStart(state.layers.entities, layer);
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -145,8 +146,8 @@ export const layersReducers = {
if (!layer) { if (!layer) {
return; return;
} }
moveToStart(state.layers, layer); moveToStart(state.layers.entities, layer);
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerBrushLineAdded: { layerBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
@ -165,7 +166,7 @@ export const layersReducers = {
clip, clip,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
prepare: (payload: BrushLineAddedArg) => ({ prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
@ -187,7 +188,7 @@ export const layersReducers = {
clip, clip,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
prepare: (payload: EraserLineAddedArg) => ({ prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
@ -205,7 +206,7 @@ export const layersReducers = {
} }
lastObject.points.push(...point); lastObject.points.push(...point);
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
layerRectAdded: { layerRectAdded: {
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => { reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
@ -225,7 +226,7 @@ export const layersReducers = {
color, color,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
}, },
@ -238,8 +239,11 @@ export const layersReducers = {
} }
layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO)); layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO));
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
state.baseLayerImageCache = null; state.layers.baseLayerImageCache = null;
}, },
prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }), prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }),
}, },
baseLayerImageCacheChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.layers.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null;
},
} satisfies SliceCaseReducers<CanvasV2State>; } satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -26,7 +26,7 @@ import type {
} from './types'; } from './types';
import { isLine } 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) => { export const selectRGOrThrow = (state: CanvasV2State, id: string) => {
const rg = selectRG(state, id); const rg = selectRG(state, id);
assert(rg, `Region with id ${id} not found`); assert(rg, `Region with id ${id} not found`);
@ -44,7 +44,7 @@ const DEFAULT_MASK_COLORS: RgbColor[] = [
]; ];
const getRGMaskFill = (state: CanvasV2State): 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)); let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
if (i === -1) { if (i === -1) {
i = 0; i = 0;
@ -75,7 +75,7 @@ export const regionsReducers = {
ipAdapters: [], ipAdapters: [],
imageCache: null, imageCache: null,
}; };
state.regions.push(rg); state.regions.entities.push(rg);
state.selectedEntityIdentifier = { type: 'regional_guidance', id }; state.selectedEntityIdentifier = { type: 'regional_guidance', id };
}, },
prepare: () => ({ payload: { id: uuidv4() } }), prepare: () => ({ payload: { id: uuidv4() } }),
@ -93,7 +93,7 @@ export const regionsReducers = {
}, },
rgRecalled: (state, action: PayloadAction<{ data: RegionEntity }>) => { rgRecalled: (state, action: PayloadAction<{ data: RegionEntity }>) => {
const { data } = action.payload; const { data } = action.payload;
state.regions.push(data); state.regions.entities.push(data);
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
}, },
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
@ -121,10 +121,10 @@ export const regionsReducers = {
}, },
rgDeleted: (state, action: PayloadAction<{ id: string }>) => { rgDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; 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) => { rgAllDeleted: (state) => {
state.regions = []; state.regions.entities = [];
}, },
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -132,7 +132,7 @@ export const regionsReducers = {
if (!rg) { if (!rg) {
return; return;
} }
moveOneToEnd(state.regions, rg); moveOneToEnd(state.regions.entities, rg);
}, },
rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -140,7 +140,7 @@ export const regionsReducers = {
if (!rg) { if (!rg) {
return; return;
} }
moveToEnd(state.regions, rg); moveToEnd(state.regions.entities, rg);
}, },
rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -148,7 +148,7 @@ export const regionsReducers = {
if (!rg) { if (!rg) {
return; return;
} }
moveOneToStart(state.regions, rg); moveOneToStart(state.regions.entities, rg);
}, },
rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
@ -156,7 +156,7 @@ export const regionsReducers = {
if (!rg) { if (!rg) {
return; return;
} }
moveToStart(state.regions, rg); moveToStart(state.regions.entities, rg);
}, },
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload; const { id, prompt } = action.payload;

View File

@ -4,7 +4,10 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => {
return ( 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
); );
}); });

View File

@ -796,10 +796,13 @@ export type CanvasV2State = {
_version: 3; _version: 3;
selectedEntityIdentifier: CanvasEntityIdentifier | null; selectedEntityIdentifier: CanvasEntityIdentifier | null;
inpaintMask: InpaintMaskEntity; inpaintMask: InpaintMaskEntity;
layers: LayerEntity[]; layers: {
controlAdapters: ControlAdapterEntity[]; baseLayerImageCache: ImageWithDims | null;
ipAdapters: IPAdapterEntity[]; entities: LayerEntity[];
regions: RegionEntity[]; };
controlAdapters: { entities: ControlAdapterEntity[] };
ipAdapters: { entities: IPAdapterEntity[] };
regions: { entities: RegionEntity[] };
loras: LoRA[]; loras: LoRA[];
tool: { tool: {
selected: Tool; selected: Tool;
@ -872,7 +875,6 @@ export type CanvasV2State = {
refinerNegativeAestheticScore: number; refinerNegativeAestheticScore: number;
refinerStart: number; refinerStart: number;
}; };
baseLayerImageCache: ImageWithDims | null;
}; };
export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number };

View File

@ -11,7 +11,7 @@ import { some } from 'lodash-es';
import type { ImageUsage } from './types'; import type { ImageUsage } from './types';
export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { 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) 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) some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name)
); );
const isControlAdapterImage = canvasV2.controlAdapters.some( const isControlAdapterImage = canvasV2.controlAdapters.entities.some(
(ca) => ca.image?.name === image_name || ca.processedImage?.name === image_name (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 = { const imageUsage: ImageUsage = {
isLayerImage, isLayerImage,

View File

@ -78,13 +78,13 @@ const getBaseLayer = async (layers: LayerEntity[], bbox: IRect, preview: boolean
export const getBaseLayerImage = async (): Promise<ImageDTO> => { export const getBaseLayerImage = async (): Promise<ImageDTO> => {
const { dispatch, getState } = getStore(); const { dispatch, getState } = getStore();
const state = getState(); const state = getState();
if (state.canvasV2.baseLayerImageCache) { if (state.canvasV2.layers.baseLayerImageCache) {
const imageDTO = await getImageDTO(state.canvasV2.baseLayerImageCache.name); const imageDTO = await getImageDTO(state.canvasV2.layers.baseLayerImageCache.name);
if (imageDTO) { if (imageDTO) {
return 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 file = new File([blob], 'image.png', { type: 'image/png' });
const req = dispatch( const req = dispatch(
imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'general', is_intermediate: true }) imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'general', is_intermediate: true })

View File

@ -156,10 +156,10 @@ export const buildGenerationTabGraph = async (state: RootState): Promise<GraphTy
const vaeSource = seamless ?? vaeLoader ?? modelLoader; const vaeSource = seamless ?? vaeLoader ?? modelLoader;
g.addEdge(vaeSource, 'vae', l2i, 'vae'); g.addEdge(vaeSource, 'vae', l2i, 'vae');
const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters, g, denoise, modelConfig.base); const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base);
const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters, g, denoise, modelConfig.base); const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base);
const _addedRegions = await addRegions( const _addedRegions = await addRegions(
state.canvasV2.regions, state.canvasV2.regions.entities,
g, g,
state.canvasV2.document, state.canvasV2.document,
state.canvasV2.bbox, state.canvasV2.bbox,

View File

@ -152,10 +152,10 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise<Non
await addSDXLRefiner(state, g, denoise, seamless, posCond, negCond, l2i); await addSDXLRefiner(state, g, denoise, seamless, posCond, negCond, l2i);
} }
const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters, g, denoise, modelConfig.base); const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base);
const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters, g, denoise, modelConfig.base); const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base);
const _addedRegions = await addRegions( const _addedRegions = await addRegions(
state.canvasV2.regions, state.canvasV2.regions.entities,
g, g,
state.canvasV2.document, state.canvasV2.document,
state.canvasV2.bbox, state.canvasV2.bbox,

View File

@ -5,6 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent'; import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
import { selectEntityCount } from 'features/controlLayers/store/selectors';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls'; import QueueControls from 'features/queue/components/QueueControls';
@ -41,7 +42,7 @@ const selectedStyles: ChakraProps['sx'] = {
const ParametersPanelTextToImage = () => { const ParametersPanelTextToImage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); const controlLayersCount = useAppSelector(selectEntityCount);
const controlLayersTitle = useMemo(() => { const controlLayersTitle = useMemo(() => {
if (controlLayersCount === 0) { if (controlLayersCount === 0) {
return t('controlLayers.controlLayers'); return t('controlLayers.controlLayers');