mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
tidy(ui): move all undoable reducers back to canvas slice
This commit is contained in:
parent
fe3b2ed357
commit
23a98e2ed6
@ -1,119 +0,0 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
|
||||||
import { deepClone } from 'common/util/deepClone';
|
|
||||||
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
|
||||||
import type { BoundingBoxScaleMethod, CanvasState, Dimensions } from 'features/controlLayers/store/types';
|
|
||||||
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
|
||||||
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
|
||||||
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
|
|
||||||
import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types';
|
|
||||||
import type { IRect } from 'konva/lib/types';
|
|
||||||
|
|
||||||
const syncScaledSize = (state: CanvasState) => {
|
|
||||||
if (state.bbox.scaleMethod === 'auto') {
|
|
||||||
const { width, height } = state.bbox.rect;
|
|
||||||
state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bboxReducers = {
|
|
||||||
bboxScaledSizeChanged: (state, action: PayloadAction<Partial<Dimensions>>) => {
|
|
||||||
state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload };
|
|
||||||
},
|
|
||||||
bboxScaleMethodChanged: (state, action: PayloadAction<BoundingBoxScaleMethod>) => {
|
|
||||||
state.bbox.scaleMethod = action.payload;
|
|
||||||
syncScaledSize(state);
|
|
||||||
},
|
|
||||||
bboxChanged: (state, action: PayloadAction<IRect>) => {
|
|
||||||
state.bbox.rect = action.payload;
|
|
||||||
syncScaledSize(state);
|
|
||||||
},
|
|
||||||
bboxWidthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
|
|
||||||
const { width, updateAspectRatio, clamp } = action.payload;
|
|
||||||
state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
|
|
||||||
|
|
||||||
if (state.bbox.aspectRatio.isLocked) {
|
|
||||||
state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) {
|
|
||||||
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
|
|
||||||
state.bbox.aspectRatio.id = 'Free';
|
|
||||||
state.bbox.aspectRatio.isLocked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncScaledSize(state);
|
|
||||||
},
|
|
||||||
bboxHeightChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>
|
|
||||||
) => {
|
|
||||||
const { height, updateAspectRatio, clamp } = action.payload;
|
|
||||||
|
|
||||||
state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height;
|
|
||||||
|
|
||||||
if (state.bbox.aspectRatio.isLocked) {
|
|
||||||
state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) {
|
|
||||||
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
|
|
||||||
state.bbox.aspectRatio.id = 'Free';
|
|
||||||
state.bbox.aspectRatio.isLocked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncScaledSize(state);
|
|
||||||
},
|
|
||||||
bboxAspectRatioLockToggled: (state) => {
|
|
||||||
state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked;
|
|
||||||
},
|
|
||||||
bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => {
|
|
||||||
const { id } = action.payload;
|
|
||||||
state.bbox.aspectRatio.id = id;
|
|
||||||
if (id === 'Free') {
|
|
||||||
state.bbox.aspectRatio.isLocked = false;
|
|
||||||
} else {
|
|
||||||
state.bbox.aspectRatio.isLocked = true;
|
|
||||||
state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio;
|
|
||||||
const { width, height } = calculateNewSize(
|
|
||||||
state.bbox.aspectRatio.value,
|
|
||||||
state.bbox.rect.width * state.bbox.rect.height
|
|
||||||
);
|
|
||||||
state.bbox.rect.width = width;
|
|
||||||
state.bbox.rect.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncScaledSize(state);
|
|
||||||
},
|
|
||||||
bboxDimensionsSwapped: (state) => {
|
|
||||||
state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value;
|
|
||||||
if (state.bbox.aspectRatio.id === 'Free') {
|
|
||||||
const newWidth = state.bbox.rect.height;
|
|
||||||
const newHeight = state.bbox.rect.width;
|
|
||||||
state.bbox.rect.width = newWidth;
|
|
||||||
state.bbox.rect.height = newHeight;
|
|
||||||
} else {
|
|
||||||
const { width, height } = calculateNewSize(
|
|
||||||
state.bbox.aspectRatio.value,
|
|
||||||
state.bbox.rect.width * state.bbox.rect.height
|
|
||||||
);
|
|
||||||
state.bbox.rect.width = width;
|
|
||||||
state.bbox.rect.height = height;
|
|
||||||
state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncScaledSize(state);
|
|
||||||
},
|
|
||||||
bboxSizeOptimized: (state) => {
|
|
||||||
if (state.bbox.aspectRatio.isLocked) {
|
|
||||||
const { width, height } = calculateNewSize(state.bbox.aspectRatio.value, state.bbox.optimalDimension ** 2);
|
|
||||||
state.bbox.rect.width = width;
|
|
||||||
state.bbox.rect.height = height;
|
|
||||||
} else {
|
|
||||||
state.bbox.aspectRatio = deepClone(initialAspectRatioState);
|
|
||||||
state.bbox.rect.width = state.bbox.optimalDimension;
|
|
||||||
state.bbox.rect.height = state.bbox.optimalDimension;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncScaledSize(state);
|
|
||||||
},
|
|
||||||
} satisfies SliceCaseReducers<CanvasState>;
|
|
@ -3,34 +3,83 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
import type { PersistConfig } from 'app/store/store';
|
import type { PersistConfig } from 'app/store/store';
|
||||||
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
|
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
|
|
||||||
import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers';
|
|
||||||
import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers';
|
|
||||||
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
|
|
||||||
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
|
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
|
||||||
import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers';
|
import {
|
||||||
import { regionsReducers } from 'features/controlLayers/store/regionsReducers';
|
selectAllEntities,
|
||||||
import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors';
|
selectAllEntitiesOfType,
|
||||||
|
selectEntity,
|
||||||
|
selectRegionalGuidanceIPAdapter,
|
||||||
|
} from 'features/controlLayers/store/selectors';
|
||||||
|
import type {
|
||||||
|
CanvasInpaintMaskState,
|
||||||
|
FillStyle,
|
||||||
|
RegionalGuidanceIPAdapterConfig,
|
||||||
|
RgbColor,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||||
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
|
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
|
||||||
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
||||||
import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
|
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
|
||||||
|
import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types';
|
||||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import { pick } from 'lodash-es';
|
import type { IRect } from 'konva/lib/types';
|
||||||
|
import { isEqual, merge, omit } from 'lodash-es';
|
||||||
|
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
BoundingBoxScaleMethod,
|
||||||
|
CanvasControlLayerState,
|
||||||
CanvasEntityIdentifier,
|
CanvasEntityIdentifier,
|
||||||
|
CanvasIPAdapterState,
|
||||||
|
CanvasRasterLayerState,
|
||||||
|
CanvasRegionalGuidanceState,
|
||||||
CanvasState,
|
CanvasState,
|
||||||
|
CLIPVisionModelV2,
|
||||||
|
ControlModeV2,
|
||||||
|
ControlNetConfig,
|
||||||
|
Dimensions,
|
||||||
EntityBrushLineAddedPayload,
|
EntityBrushLineAddedPayload,
|
||||||
EntityEraserLineAddedPayload,
|
EntityEraserLineAddedPayload,
|
||||||
EntityIdentifierPayload,
|
EntityIdentifierPayload,
|
||||||
EntityMovedPayload,
|
EntityMovedPayload,
|
||||||
EntityRasterizedPayload,
|
EntityRasterizedPayload,
|
||||||
EntityRectAddedPayload,
|
EntityRectAddedPayload,
|
||||||
|
IPMethodV2,
|
||||||
|
T2IAdapterConfig,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getEntityIdentifier, isDrawableEntity } from './types';
|
import {
|
||||||
|
getEntityIdentifier,
|
||||||
|
imageDTOToImageWithDims,
|
||||||
|
initialControlNet,
|
||||||
|
initialIPAdapter,
|
||||||
|
isDrawableEntity,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const DEFAULT_MASK_COLORS: RgbColor[] = [
|
||||||
|
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
|
||||||
|
{ r: 131, g: 214, b: 131 }, // rgb(131, 214, 131)
|
||||||
|
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
|
||||||
|
{ r: 220, g: 144, b: 101 }, // rgb(220, 144, 101)
|
||||||
|
{ r: 224, g: 117, b: 117 }, // rgb(224, 117, 117)
|
||||||
|
{ r: 213, g: 139, b: 202 }, // rgb(213, 139, 202)
|
||||||
|
{ r: 161, g: 120, b: 214 }, // rgb(161, 120, 214)
|
||||||
|
];
|
||||||
|
|
||||||
|
const getRGMaskFill = (state: CanvasState): RgbColor => {
|
||||||
|
const lastFill = state.regions.entities.slice(-1)[0]?.fill.color;
|
||||||
|
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
|
||||||
|
if (i === -1) {
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
i = (i + 1) % DEFAULT_MASK_COLORS.length;
|
||||||
|
const fill = DEFAULT_MASK_COLORS[i];
|
||||||
|
assert(fill, 'This should never happen');
|
||||||
|
return fill;
|
||||||
|
};
|
||||||
|
|
||||||
const initialState: CanvasState = {
|
const initialState: CanvasState = {
|
||||||
_version: 3,
|
_version: 3,
|
||||||
@ -69,12 +118,667 @@ export const canvasSlice = createSlice({
|
|||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
// undoable canvas state
|
// undoable canvas state
|
||||||
...rasterLayersReducers,
|
//#region Raster layers
|
||||||
...controlLayersReducers,
|
rasterLayerAdded: {
|
||||||
...ipAdaptersReducers,
|
reducer: (
|
||||||
...regionsReducers,
|
state,
|
||||||
...inpaintMaskReducers,
|
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }>
|
||||||
...bboxReducers,
|
) => {
|
||||||
|
const { id, overrides, isSelected } = action.payload;
|
||||||
|
const entity: CanvasRasterLayerState = {
|
||||||
|
id,
|
||||||
|
name: null,
|
||||||
|
type: 'raster_layer',
|
||||||
|
isEnabled: true,
|
||||||
|
objects: [],
|
||||||
|
opacity: 1,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
};
|
||||||
|
merge(entity, overrides);
|
||||||
|
state.rasterLayers.entities.push(entity);
|
||||||
|
if (isSelected) {
|
||||||
|
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: { overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }) => ({
|
||||||
|
payload: { ...payload, id: getPrefixedId('raster_layer') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => {
|
||||||
|
const { data } = action.payload;
|
||||||
|
state.rasterLayers.entities.push(data);
|
||||||
|
state.selectedEntityIdentifier = getEntityIdentifier(data);
|
||||||
|
},
|
||||||
|
rasterLayerConvertedToControlLayer: {
|
||||||
|
reducer: (state, action: PayloadAction<EntityIdentifierPayload<{ newId: string }, 'raster_layer'>>) => {
|
||||||
|
const { entityIdentifier, newId } = action.payload;
|
||||||
|
const layer = selectEntity(state, entityIdentifier);
|
||||||
|
if (!layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the raster layer to control layer
|
||||||
|
const controlLayerState: CanvasControlLayerState = {
|
||||||
|
...deepClone(layer),
|
||||||
|
id: newId,
|
||||||
|
type: 'control_layer',
|
||||||
|
controlAdapter: deepClone(initialControlNet),
|
||||||
|
withTransparencyEffect: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the raster layer
|
||||||
|
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
|
||||||
|
|
||||||
|
// Add the converted control layer
|
||||||
|
state.controlLayers.entities.push(controlLayerState);
|
||||||
|
|
||||||
|
state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id };
|
||||||
|
},
|
||||||
|
prepare: (payload: EntityIdentifierPayload<void, 'raster_layer'>) => ({
|
||||||
|
payload: { ...payload, newId: getPrefixedId('control_layer') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
//#region Control layers
|
||||||
|
controlLayerAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ id: string; overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }>
|
||||||
|
) => {
|
||||||
|
const { id, overrides, isSelected } = action.payload;
|
||||||
|
const entity: CanvasControlLayerState = {
|
||||||
|
id,
|
||||||
|
name: null,
|
||||||
|
type: 'control_layer',
|
||||||
|
isEnabled: true,
|
||||||
|
withTransparencyEffect: true,
|
||||||
|
objects: [],
|
||||||
|
opacity: 1,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
controlAdapter: deepClone(initialControlNet),
|
||||||
|
};
|
||||||
|
merge(entity, overrides);
|
||||||
|
state.controlLayers.entities.push(entity);
|
||||||
|
if (isSelected) {
|
||||||
|
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: { overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }) => ({
|
||||||
|
payload: { ...payload, id: getPrefixedId('control_layer') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
controlLayerRecalled: (state, action: PayloadAction<{ data: CanvasControlLayerState }>) => {
|
||||||
|
const { data } = action.payload;
|
||||||
|
state.controlLayers.entities.push(data);
|
||||||
|
state.selectedEntityIdentifier = { type: 'control_layer', id: data.id };
|
||||||
|
},
|
||||||
|
controlLayerConvertedToRasterLayer: {
|
||||||
|
reducer: (state, action: PayloadAction<EntityIdentifierPayload<{ newId: string }, 'control_layer'>>) => {
|
||||||
|
const { entityIdentifier, newId } = action.payload;
|
||||||
|
const layer = selectEntity(state, entityIdentifier);
|
||||||
|
if (!layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the raster layer to control layer
|
||||||
|
const rasterLayerState: CanvasRasterLayerState = {
|
||||||
|
...omit(deepClone(layer), ['type', 'controlAdapter', 'withTransparencyEffect']),
|
||||||
|
id: newId,
|
||||||
|
type: 'raster_layer',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the control layer
|
||||||
|
state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
|
||||||
|
|
||||||
|
// Add the new raster layer
|
||||||
|
state.rasterLayers.entities.push(rasterLayerState);
|
||||||
|
|
||||||
|
state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id };
|
||||||
|
},
|
||||||
|
prepare: (payload: EntityIdentifierPayload<void, 'control_layer'>) => ({
|
||||||
|
payload: { ...payload, newId: getPrefixedId('raster_layer') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
controlLayerModelChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
EntityIdentifierPayload<
|
||||||
|
{
|
||||||
|
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
|
||||||
|
},
|
||||||
|
'control_layer'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, modelConfig } = action.payload;
|
||||||
|
const layer = selectEntity(state, entityIdentifier);
|
||||||
|
if (!layer || !layer.controlAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!modelConfig) {
|
||||||
|
layer.controlAdapter.model = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig);
|
||||||
|
|
||||||
|
// We may need to convert the CA to match the model
|
||||||
|
if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') {
|
||||||
|
// Converting from T2I Adapter to ControlNet - add `controlMode`
|
||||||
|
const controlNetConfig: ControlNetConfig = {
|
||||||
|
...layer.controlAdapter,
|
||||||
|
type: 'controlnet',
|
||||||
|
controlMode: 'balanced',
|
||||||
|
};
|
||||||
|
layer.controlAdapter = controlNetConfig;
|
||||||
|
} else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') {
|
||||||
|
// Converting from ControlNet to T2I Adapter - remove `controlMode`
|
||||||
|
const { controlMode: _, ...rest } = layer.controlAdapter;
|
||||||
|
const t2iAdapterConfig: T2IAdapterConfig = { ...rest, type: 't2i_adapter' };
|
||||||
|
layer.controlAdapter = t2iAdapterConfig;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controlLayerControlModeChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ controlMode: ControlModeV2 }, 'control_layer'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, controlMode } = action.payload;
|
||||||
|
const layer = selectEntity(state, entityIdentifier);
|
||||||
|
if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.controlAdapter.controlMode = controlMode;
|
||||||
|
},
|
||||||
|
controlLayerWeightChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'control_layer'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, weight } = action.payload;
|
||||||
|
const layer = selectEntity(state, entityIdentifier);
|
||||||
|
if (!layer || !layer.controlAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.controlAdapter.weight = weight;
|
||||||
|
},
|
||||||
|
controlLayerBeginEndStepPctChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'control_layer'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, beginEndStepPct } = action.payload;
|
||||||
|
const layer = selectEntity(state, entityIdentifier);
|
||||||
|
if (!layer || !layer.controlAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
|
||||||
|
},
|
||||||
|
controlLayerWithTransparencyEffectToggled: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<void, 'control_layer'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier } = action.payload;
|
||||||
|
const layer = selectEntity(state, entityIdentifier);
|
||||||
|
if (!layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.withTransparencyEffect = !layer.withTransparencyEffect;
|
||||||
|
},
|
||||||
|
//#region IP Adapters
|
||||||
|
ipaAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ id: string; overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }>
|
||||||
|
) => {
|
||||||
|
const { id, overrides, isSelected } = action.payload;
|
||||||
|
const entity: CanvasIPAdapterState = {
|
||||||
|
id,
|
||||||
|
type: 'ip_adapter',
|
||||||
|
name: null,
|
||||||
|
isEnabled: true,
|
||||||
|
ipAdapter: deepClone(initialIPAdapter),
|
||||||
|
};
|
||||||
|
merge(entity, overrides);
|
||||||
|
state.ipAdapters.entities.push(entity);
|
||||||
|
if (isSelected) {
|
||||||
|
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload?: { overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }) => ({
|
||||||
|
payload: { ...payload, id: getPrefixedId('ip_adapter') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => {
|
||||||
|
const { data } = action.payload;
|
||||||
|
state.ipAdapters.entities.push(data);
|
||||||
|
state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id };
|
||||||
|
},
|
||||||
|
ipaImageChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ imageDTO: ImageDTO | null }, 'ip_adapter'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, imageDTO } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||||
|
},
|
||||||
|
ipaMethodChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ method: IPMethodV2 }, 'ip_adapter'>>) => {
|
||||||
|
const { entityIdentifier, method } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.ipAdapter.method = method;
|
||||||
|
},
|
||||||
|
ipaModelChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ modelConfig: IPAdapterModelConfig | null }, 'ip_adapter'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, modelConfig } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
|
||||||
|
},
|
||||||
|
ipaCLIPVisionModelChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ clipVisionModel: CLIPVisionModelV2 }, 'ip_adapter'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, clipVisionModel } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.ipAdapter.clipVisionModel = clipVisionModel;
|
||||||
|
},
|
||||||
|
ipaWeightChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'ip_adapter'>>) => {
|
||||||
|
const { entityIdentifier, weight } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.ipAdapter.weight = weight;
|
||||||
|
},
|
||||||
|
ipaBeginEndStepPctChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'ip_adapter'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, beginEndStepPct } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.ipAdapter.beginEndStepPct = beginEndStepPct;
|
||||||
|
},
|
||||||
|
//#region Regional Guidance
|
||||||
|
rgAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }>
|
||||||
|
) => {
|
||||||
|
const { id, overrides, isSelected } = action.payload;
|
||||||
|
const entity: CanvasRegionalGuidanceState = {
|
||||||
|
id,
|
||||||
|
name: null,
|
||||||
|
type: 'regional_guidance',
|
||||||
|
isEnabled: true,
|
||||||
|
objects: [],
|
||||||
|
fill: {
|
||||||
|
style: 'solid',
|
||||||
|
color: getRGMaskFill(state),
|
||||||
|
},
|
||||||
|
opacity: 0.5,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
autoNegative: true,
|
||||||
|
positivePrompt: '',
|
||||||
|
negativePrompt: null,
|
||||||
|
ipAdapters: [],
|
||||||
|
};
|
||||||
|
merge(entity, overrides);
|
||||||
|
state.regions.entities.push(entity);
|
||||||
|
if (isSelected) {
|
||||||
|
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload?: { overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }) => ({
|
||||||
|
payload: { ...payload, id: getPrefixedId('regional_guidance') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => {
|
||||||
|
const { data } = action.payload;
|
||||||
|
state.regions.entities.push(data);
|
||||||
|
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
|
||||||
|
},
|
||||||
|
rgPositivePromptChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ prompt: string | null }, 'regional_guidance'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, prompt } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.positivePrompt = prompt;
|
||||||
|
},
|
||||||
|
rgNegativePromptChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ prompt: string | null }, 'regional_guidance'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, prompt } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.negativePrompt = prompt;
|
||||||
|
},
|
||||||
|
rgFillColorChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'regional_guidance'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, color } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.fill.color = color;
|
||||||
|
},
|
||||||
|
rgFillStyleChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'regional_guidance'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, style } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.fill.style = style;
|
||||||
|
},
|
||||||
|
|
||||||
|
rgAutoNegativeToggled: (state, action: PayloadAction<EntityIdentifierPayload<void, 'regional_guidance'>>) => {
|
||||||
|
const { entityIdentifier } = action.payload;
|
||||||
|
const rg = selectEntity(state, entityIdentifier);
|
||||||
|
if (!rg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rg.autoNegative = !rg.autoNegative;
|
||||||
|
},
|
||||||
|
rgIPAdapterAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
EntityIdentifierPayload<
|
||||||
|
{ ipAdapterId: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> },
|
||||||
|
'regional_guidance'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, overrides, ipAdapterId } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ipAdapter = { ...deepClone(initialIPAdapter), id: ipAdapterId };
|
||||||
|
merge(ipAdapter, overrides);
|
||||||
|
entity.ipAdapters.push(ipAdapter);
|
||||||
|
},
|
||||||
|
prepare: (
|
||||||
|
payload: EntityIdentifierPayload<{ overrides?: Partial<RegionalGuidanceIPAdapterConfig> }, 'regional_guidance'>
|
||||||
|
) => ({
|
||||||
|
payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
rgIPAdapterDeleted: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string }, 'regional_guidance'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, ipAdapterId } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.ipAdapters = entity.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
|
||||||
|
},
|
||||||
|
rgIPAdapterImageChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
EntityIdentifierPayload<{ ipAdapterId: string; imageDTO: ImageDTO | null }, 'regional_guidance'>
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, ipAdapterId, imageDTO } = action.payload;
|
||||||
|
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||||
|
if (!ipAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||||
|
},
|
||||||
|
rgIPAdapterWeightChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; weight: number }, 'regional_guidance'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, ipAdapterId, weight } = action.payload;
|
||||||
|
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||||
|
if (!ipAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ipAdapter.weight = weight;
|
||||||
|
},
|
||||||
|
rgIPAdapterBeginEndStepPctChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
EntityIdentifierPayload<{ ipAdapterId: string; beginEndStepPct: [number, number] }, 'regional_guidance'>
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, ipAdapterId, beginEndStepPct } = action.payload;
|
||||||
|
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||||
|
if (!ipAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ipAdapter.beginEndStepPct = beginEndStepPct;
|
||||||
|
},
|
||||||
|
rgIPAdapterMethodChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; method: IPMethodV2 }, 'regional_guidance'>>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, ipAdapterId, method } = action.payload;
|
||||||
|
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||||
|
if (!ipAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ipAdapter.method = method;
|
||||||
|
},
|
||||||
|
rgIPAdapterModelChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
EntityIdentifierPayload<
|
||||||
|
{
|
||||||
|
ipAdapterId: string;
|
||||||
|
modelConfig: IPAdapterModelConfig | null;
|
||||||
|
},
|
||||||
|
'regional_guidance'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, ipAdapterId, modelConfig } = action.payload;
|
||||||
|
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||||
|
if (!ipAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
|
||||||
|
},
|
||||||
|
rgIPAdapterCLIPVisionModelChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
EntityIdentifierPayload<{ ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'>
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { entityIdentifier, ipAdapterId, clipVisionModel } = action.payload;
|
||||||
|
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||||
|
if (!ipAdapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ipAdapter.clipVisionModel = clipVisionModel;
|
||||||
|
},
|
||||||
|
//#region Inpaint mask
|
||||||
|
inpaintMaskAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ id: string; overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }>
|
||||||
|
) => {
|
||||||
|
const { id, overrides, isSelected } = action.payload;
|
||||||
|
const entity: CanvasInpaintMaskState = {
|
||||||
|
id,
|
||||||
|
name: null,
|
||||||
|
type: 'inpaint_mask',
|
||||||
|
isEnabled: true,
|
||||||
|
objects: [],
|
||||||
|
opacity: 1,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
fill: {
|
||||||
|
style: 'diagonal',
|
||||||
|
color: { r: 255, g: 122, b: 0 }, // some orange color
|
||||||
|
},
|
||||||
|
};
|
||||||
|
merge(entity, overrides);
|
||||||
|
state.inpaintMasks.entities.push(entity);
|
||||||
|
if (isSelected) {
|
||||||
|
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload?: { overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }) => ({
|
||||||
|
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
inpaintMaskRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
|
||||||
|
const { data } = action.payload;
|
||||||
|
state.inpaintMasks.entities = [data];
|
||||||
|
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
|
||||||
|
},
|
||||||
|
inpaintMaskFillColorChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'inpaint_mask'>>
|
||||||
|
) => {
|
||||||
|
const { color, entityIdentifier } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.fill.color = color;
|
||||||
|
},
|
||||||
|
inpaintMaskFillStyleChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'inpaint_mask'>>
|
||||||
|
) => {
|
||||||
|
const { style, entityIdentifier } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.fill.style = style;
|
||||||
|
},
|
||||||
|
//#region BBox
|
||||||
|
bboxScaledSizeChanged: (state, action: PayloadAction<Partial<Dimensions>>) => {
|
||||||
|
state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload };
|
||||||
|
},
|
||||||
|
bboxScaleMethodChanged: (state, action: PayloadAction<BoundingBoxScaleMethod>) => {
|
||||||
|
state.bbox.scaleMethod = action.payload;
|
||||||
|
syncScaledSize(state);
|
||||||
|
},
|
||||||
|
bboxChanged: (state, action: PayloadAction<IRect>) => {
|
||||||
|
state.bbox.rect = action.payload;
|
||||||
|
syncScaledSize(state);
|
||||||
|
},
|
||||||
|
bboxWidthChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>
|
||||||
|
) => {
|
||||||
|
const { width, updateAspectRatio, clamp } = action.payload;
|
||||||
|
state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
|
||||||
|
|
||||||
|
if (state.bbox.aspectRatio.isLocked) {
|
||||||
|
state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) {
|
||||||
|
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
|
||||||
|
state.bbox.aspectRatio.id = 'Free';
|
||||||
|
state.bbox.aspectRatio.isLocked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncScaledSize(state);
|
||||||
|
},
|
||||||
|
bboxHeightChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>
|
||||||
|
) => {
|
||||||
|
const { height, updateAspectRatio, clamp } = action.payload;
|
||||||
|
|
||||||
|
state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height;
|
||||||
|
|
||||||
|
if (state.bbox.aspectRatio.isLocked) {
|
||||||
|
state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) {
|
||||||
|
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
|
||||||
|
state.bbox.aspectRatio.id = 'Free';
|
||||||
|
state.bbox.aspectRatio.isLocked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncScaledSize(state);
|
||||||
|
},
|
||||||
|
bboxAspectRatioLockToggled: (state) => {
|
||||||
|
state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked;
|
||||||
|
},
|
||||||
|
bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => {
|
||||||
|
const { id } = action.payload;
|
||||||
|
state.bbox.aspectRatio.id = id;
|
||||||
|
if (id === 'Free') {
|
||||||
|
state.bbox.aspectRatio.isLocked = false;
|
||||||
|
} else {
|
||||||
|
state.bbox.aspectRatio.isLocked = true;
|
||||||
|
state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio;
|
||||||
|
const { width, height } = calculateNewSize(
|
||||||
|
state.bbox.aspectRatio.value,
|
||||||
|
state.bbox.rect.width * state.bbox.rect.height
|
||||||
|
);
|
||||||
|
state.bbox.rect.width = width;
|
||||||
|
state.bbox.rect.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncScaledSize(state);
|
||||||
|
},
|
||||||
|
bboxDimensionsSwapped: (state) => {
|
||||||
|
state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value;
|
||||||
|
if (state.bbox.aspectRatio.id === 'Free') {
|
||||||
|
const newWidth = state.bbox.rect.height;
|
||||||
|
const newHeight = state.bbox.rect.width;
|
||||||
|
state.bbox.rect.width = newWidth;
|
||||||
|
state.bbox.rect.height = newHeight;
|
||||||
|
} else {
|
||||||
|
const { width, height } = calculateNewSize(
|
||||||
|
state.bbox.aspectRatio.value,
|
||||||
|
state.bbox.rect.width * state.bbox.rect.height
|
||||||
|
);
|
||||||
|
state.bbox.rect.width = width;
|
||||||
|
state.bbox.rect.height = height;
|
||||||
|
state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncScaledSize(state);
|
||||||
|
},
|
||||||
|
bboxSizeOptimized: (state) => {
|
||||||
|
if (state.bbox.aspectRatio.isLocked) {
|
||||||
|
const { width, height } = calculateNewSize(state.bbox.aspectRatio.value, state.bbox.optimalDimension ** 2);
|
||||||
|
state.bbox.rect.width = width;
|
||||||
|
state.bbox.rect.height = height;
|
||||||
|
} else {
|
||||||
|
state.bbox.aspectRatio = deepClone(initialAspectRatioState);
|
||||||
|
state.bbox.rect.width = state.bbox.optimalDimension;
|
||||||
|
state.bbox.rect.height = state.bbox.optimalDimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncScaledSize(state);
|
||||||
|
},
|
||||||
|
//#region Shared entity
|
||||||
entitySelected: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
entitySelected: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
||||||
const { entityIdentifier } = action.payload;
|
const { entityIdentifier } = action.payload;
|
||||||
state.selectedEntityIdentifier = entityIdentifier;
|
state.selectedEntityIdentifier = entityIdentifier;
|
||||||
@ -322,19 +1026,11 @@ export const canvasSlice = createSlice({
|
|||||||
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
|
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
|
||||||
},
|
},
|
||||||
canvasReset: (state) => {
|
canvasReset: (state) => {
|
||||||
state.bbox = deepClone(initialState.bbox);
|
const { width, height } = state.bbox.rect;
|
||||||
state.bbox.rect.width = state.bbox.optimalDimension;
|
const scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension);
|
||||||
state.bbox.rect.height = state.bbox.optimalDimension;
|
const newState = deepClone(initialState);
|
||||||
const size = pick(state.bbox.rect, 'width', 'height');
|
newState.bbox.scaledSize = scaledSize;
|
||||||
state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, state.bbox.optimalDimension);
|
return newState;
|
||||||
|
|
||||||
state.ipAdapters = deepClone(initialState.ipAdapters);
|
|
||||||
state.rasterLayers = deepClone(initialState.rasterLayers);
|
|
||||||
state.controlLayers = deepClone(initialState.controlLayers);
|
|
||||||
state.regions = deepClone(initialState.regions);
|
|
||||||
state.inpaintMasks = deepClone(initialState.inpaintMasks);
|
|
||||||
|
|
||||||
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
@ -451,3 +1147,10 @@ export const canvasPersistConfig: PersistConfig<CanvasState> = {
|
|||||||
migrate,
|
migrate,
|
||||||
persistDenylist: [],
|
persistDenylist: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const syncScaledSize = (state: CanvasState) => {
|
||||||
|
if (state.bbox.scaleMethod === 'auto') {
|
||||||
|
const { width, height } = state.bbox.rect;
|
||||||
|
state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,162 +0,0 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
|
||||||
import { deepClone } from 'common/util/deepClone';
|
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
|
||||||
import { selectEntity } from 'features/controlLayers/store/selectors';
|
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
|
||||||
import { merge, omit } from 'lodash-es';
|
|
||||||
import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CanvasControlLayerState,
|
|
||||||
CanvasRasterLayerState,
|
|
||||||
CanvasState,
|
|
||||||
ControlModeV2,
|
|
||||||
ControlNetConfig,
|
|
||||||
EntityIdentifierPayload,
|
|
||||||
T2IAdapterConfig,
|
|
||||||
} from './types';
|
|
||||||
import { getEntityIdentifier, initialControlNet } from './types';
|
|
||||||
|
|
||||||
export const controlLayersReducers = {
|
|
||||||
controlLayerAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }>
|
|
||||||
) => {
|
|
||||||
const { id, overrides, isSelected } = action.payload;
|
|
||||||
const entity: CanvasControlLayerState = {
|
|
||||||
id,
|
|
||||||
name: null,
|
|
||||||
type: 'control_layer',
|
|
||||||
isEnabled: true,
|
|
||||||
withTransparencyEffect: true,
|
|
||||||
objects: [],
|
|
||||||
opacity: 1,
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
controlAdapter: deepClone(initialControlNet),
|
|
||||||
};
|
|
||||||
merge(entity, overrides);
|
|
||||||
state.controlLayers.entities.push(entity);
|
|
||||||
if (isSelected) {
|
|
||||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload: { overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }) => ({
|
|
||||||
payload: { ...payload, id: getPrefixedId('control_layer') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
controlLayerRecalled: (state, action: PayloadAction<{ data: CanvasControlLayerState }>) => {
|
|
||||||
const { data } = action.payload;
|
|
||||||
state.controlLayers.entities.push(data);
|
|
||||||
state.selectedEntityIdentifier = { type: 'control_layer', id: data.id };
|
|
||||||
},
|
|
||||||
controlLayerConvertedToRasterLayer: {
|
|
||||||
reducer: (state, action: PayloadAction<EntityIdentifierPayload<{ newId: string }, 'control_layer'>>) => {
|
|
||||||
const { entityIdentifier, newId } = action.payload;
|
|
||||||
const layer = selectEntity(state, entityIdentifier);
|
|
||||||
if (!layer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the raster layer to control layer
|
|
||||||
const rasterLayerState: CanvasRasterLayerState = {
|
|
||||||
...omit(deepClone(layer), ['type', 'controlAdapter', 'withTransparencyEffect']),
|
|
||||||
id: newId,
|
|
||||||
type: 'raster_layer',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove the control layer
|
|
||||||
state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
|
|
||||||
|
|
||||||
// Add the new raster layer
|
|
||||||
state.rasterLayers.entities.push(rasterLayerState);
|
|
||||||
|
|
||||||
state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id };
|
|
||||||
},
|
|
||||||
prepare: (payload: EntityIdentifierPayload<void, 'control_layer'>) => ({
|
|
||||||
payload: { ...payload, newId: getPrefixedId('raster_layer') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
controlLayerModelChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
EntityIdentifierPayload<
|
|
||||||
{
|
|
||||||
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
|
|
||||||
},
|
|
||||||
'control_layer'
|
|
||||||
>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, modelConfig } = action.payload;
|
|
||||||
const layer = selectEntity(state, entityIdentifier);
|
|
||||||
if (!layer || !layer.controlAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!modelConfig) {
|
|
||||||
layer.controlAdapter.model = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig);
|
|
||||||
|
|
||||||
// We may need to convert the CA to match the model
|
|
||||||
if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') {
|
|
||||||
// Converting from T2I Adapter to ControlNet - add `controlMode`
|
|
||||||
const controlNetConfig: ControlNetConfig = {
|
|
||||||
...layer.controlAdapter,
|
|
||||||
type: 'controlnet',
|
|
||||||
controlMode: 'balanced',
|
|
||||||
};
|
|
||||||
layer.controlAdapter = controlNetConfig;
|
|
||||||
} else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') {
|
|
||||||
// Converting from ControlNet to T2I Adapter - remove `controlMode`
|
|
||||||
const { controlMode: _, ...rest } = layer.controlAdapter;
|
|
||||||
const t2iAdapterConfig: T2IAdapterConfig = { ...rest, type: 't2i_adapter' };
|
|
||||||
layer.controlAdapter = t2iAdapterConfig;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
controlLayerControlModeChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ controlMode: ControlModeV2 }, 'control_layer'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, controlMode } = action.payload;
|
|
||||||
const layer = selectEntity(state, entityIdentifier);
|
|
||||||
if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
layer.controlAdapter.controlMode = controlMode;
|
|
||||||
},
|
|
||||||
controlLayerWeightChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'control_layer'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, weight } = action.payload;
|
|
||||||
const layer = selectEntity(state, entityIdentifier);
|
|
||||||
if (!layer || !layer.controlAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
layer.controlAdapter.weight = weight;
|
|
||||||
},
|
|
||||||
controlLayerBeginEndStepPctChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'control_layer'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, beginEndStepPct } = action.payload;
|
|
||||||
const layer = selectEntity(state, entityIdentifier);
|
|
||||||
if (!layer || !layer.controlAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
|
|
||||||
},
|
|
||||||
controlLayerWithTransparencyEffectToggled: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<void, 'control_layer'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier } = action.payload;
|
|
||||||
const layer = selectEntity(state, entityIdentifier);
|
|
||||||
if (!layer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
layer.withTransparencyEffect = !layer.withTransparencyEffect;
|
|
||||||
},
|
|
||||||
} satisfies SliceCaseReducers<CanvasState>;
|
|
@ -1,71 +0,0 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
|
||||||
import { selectEntity } from 'features/controlLayers/store/selectors';
|
|
||||||
import type {
|
|
||||||
CanvasInpaintMaskState,
|
|
||||||
CanvasState,
|
|
||||||
EntityIdentifierPayload,
|
|
||||||
FillStyle,
|
|
||||||
RgbColor,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
|
||||||
import { merge } from 'lodash-es';
|
|
||||||
|
|
||||||
export const inpaintMaskReducers = {
|
|
||||||
inpaintMaskAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }>
|
|
||||||
) => {
|
|
||||||
const { id, overrides, isSelected } = action.payload;
|
|
||||||
const entity: CanvasInpaintMaskState = {
|
|
||||||
id,
|
|
||||||
name: null,
|
|
||||||
type: 'inpaint_mask',
|
|
||||||
isEnabled: true,
|
|
||||||
objects: [],
|
|
||||||
opacity: 1,
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
fill: {
|
|
||||||
style: 'diagonal',
|
|
||||||
color: { r: 255, g: 122, b: 0 }, // some orange color
|
|
||||||
},
|
|
||||||
};
|
|
||||||
merge(entity, overrides);
|
|
||||||
state.inpaintMasks.entities.push(entity);
|
|
||||||
if (isSelected) {
|
|
||||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload?: { overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }) => ({
|
|
||||||
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
inpaintMaskRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
|
|
||||||
const { data } = action.payload;
|
|
||||||
state.inpaintMasks.entities = [data];
|
|
||||||
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
|
|
||||||
},
|
|
||||||
inpaintMaskFillColorChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'inpaint_mask'>>
|
|
||||||
) => {
|
|
||||||
const { color, entityIdentifier } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.fill.color = color;
|
|
||||||
},
|
|
||||||
inpaintMaskFillStyleChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'inpaint_mask'>>
|
|
||||||
) => {
|
|
||||||
const { style, entityIdentifier } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.fill.style = style;
|
|
||||||
},
|
|
||||||
} satisfies SliceCaseReducers<CanvasState>;
|
|
@ -1,107 +0,0 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
|
||||||
import { deepClone } from 'common/util/deepClone';
|
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
|
||||||
import { selectEntity } from 'features/controlLayers/store/selectors';
|
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
|
||||||
import { merge } from 'lodash-es';
|
|
||||||
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CanvasIPAdapterState,
|
|
||||||
CanvasState,
|
|
||||||
CLIPVisionModelV2,
|
|
||||||
EntityIdentifierPayload,
|
|
||||||
IPMethodV2,
|
|
||||||
} from './types';
|
|
||||||
import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from './types';
|
|
||||||
|
|
||||||
export const ipAdaptersReducers = {
|
|
||||||
ipaAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }>
|
|
||||||
) => {
|
|
||||||
const { id, overrides, isSelected } = action.payload;
|
|
||||||
const entity: CanvasIPAdapterState = {
|
|
||||||
id,
|
|
||||||
type: 'ip_adapter',
|
|
||||||
name: null,
|
|
||||||
isEnabled: true,
|
|
||||||
ipAdapter: deepClone(initialIPAdapter),
|
|
||||||
};
|
|
||||||
merge(entity, overrides);
|
|
||||||
state.ipAdapters.entities.push(entity);
|
|
||||||
if (isSelected) {
|
|
||||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload?: { overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }) => ({
|
|
||||||
payload: { ...payload, id: getPrefixedId('ip_adapter') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => {
|
|
||||||
const { data } = action.payload;
|
|
||||||
state.ipAdapters.entities.push(data);
|
|
||||||
state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id };
|
|
||||||
},
|
|
||||||
ipaImageChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ imageDTO: ImageDTO | null }, 'ip_adapter'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, imageDTO } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
|
||||||
},
|
|
||||||
ipaMethodChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ method: IPMethodV2 }, 'ip_adapter'>>) => {
|
|
||||||
const { entityIdentifier, method } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.ipAdapter.method = method;
|
|
||||||
},
|
|
||||||
ipaModelChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ modelConfig: IPAdapterModelConfig | null }, 'ip_adapter'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, modelConfig } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
|
|
||||||
},
|
|
||||||
ipaCLIPVisionModelChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ clipVisionModel: CLIPVisionModelV2 }, 'ip_adapter'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, clipVisionModel } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.ipAdapter.clipVisionModel = clipVisionModel;
|
|
||||||
},
|
|
||||||
ipaWeightChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'ip_adapter'>>) => {
|
|
||||||
const { entityIdentifier, weight } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.ipAdapter.weight = weight;
|
|
||||||
},
|
|
||||||
ipaBeginEndStepPctChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'ip_adapter'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, beginEndStepPct } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.ipAdapter.beginEndStepPct = beginEndStepPct;
|
|
||||||
},
|
|
||||||
} satisfies SliceCaseReducers<CanvasState>;
|
|
@ -1,70 +0,0 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
|
||||||
import { deepClone } from 'common/util/deepClone';
|
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
|
||||||
import { selectEntity } from 'features/controlLayers/store/selectors';
|
|
||||||
import { merge } from 'lodash-es';
|
|
||||||
|
|
||||||
import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasState, EntityIdentifierPayload } from './types';
|
|
||||||
import { getEntityIdentifier, initialControlNet } from './types';
|
|
||||||
|
|
||||||
export const rasterLayersReducers = {
|
|
||||||
rasterLayerAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }>
|
|
||||||
) => {
|
|
||||||
const { id, overrides, isSelected } = action.payload;
|
|
||||||
const entity: CanvasRasterLayerState = {
|
|
||||||
id,
|
|
||||||
name: null,
|
|
||||||
type: 'raster_layer',
|
|
||||||
isEnabled: true,
|
|
||||||
objects: [],
|
|
||||||
opacity: 1,
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
|
||||||
merge(entity, overrides);
|
|
||||||
state.rasterLayers.entities.push(entity);
|
|
||||||
if (isSelected) {
|
|
||||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload: { overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }) => ({
|
|
||||||
payload: { ...payload, id: getPrefixedId('raster_layer') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => {
|
|
||||||
const { data } = action.payload;
|
|
||||||
state.rasterLayers.entities.push(data);
|
|
||||||
state.selectedEntityIdentifier = getEntityIdentifier(data);
|
|
||||||
},
|
|
||||||
rasterLayerConvertedToControlLayer: {
|
|
||||||
reducer: (state, action: PayloadAction<EntityIdentifierPayload<{ newId: string }, 'raster_layer'>>) => {
|
|
||||||
const { entityIdentifier, newId } = action.payload;
|
|
||||||
const layer = selectEntity(state, entityIdentifier);
|
|
||||||
if (!layer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the raster layer to control layer
|
|
||||||
const controlLayerState: CanvasControlLayerState = {
|
|
||||||
...deepClone(layer),
|
|
||||||
id: newId,
|
|
||||||
type: 'control_layer',
|
|
||||||
controlAdapter: deepClone(initialControlNet),
|
|
||||||
withTransparencyEffect: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove the raster layer
|
|
||||||
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
|
|
||||||
|
|
||||||
// Add the converted control layer
|
|
||||||
state.controlLayers.entities.push(controlLayerState);
|
|
||||||
|
|
||||||
state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id };
|
|
||||||
},
|
|
||||||
prepare: (payload: EntityIdentifierPayload<void, 'raster_layer'>) => ({
|
|
||||||
payload: { ...payload, newId: getPrefixedId('control_layer') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
} satisfies SliceCaseReducers<CanvasState>;
|
|
@ -1,252 +0,0 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
|
||||||
import { deepClone } from 'common/util/deepClone';
|
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
|
||||||
import { selectEntity, selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors';
|
|
||||||
import type {
|
|
||||||
CanvasState,
|
|
||||||
CLIPVisionModelV2,
|
|
||||||
EntityIdentifierPayload,
|
|
||||||
FillStyle,
|
|
||||||
IPMethodV2,
|
|
||||||
RegionalGuidanceIPAdapterConfig,
|
|
||||||
RgbColor,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from 'features/controlLayers/store/types';
|
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
|
||||||
import { isEqual, merge } from 'lodash-es';
|
|
||||||
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
import type { CanvasRegionalGuidanceState } from './types';
|
|
||||||
|
|
||||||
const DEFAULT_MASK_COLORS: RgbColor[] = [
|
|
||||||
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
|
|
||||||
{ r: 131, g: 214, b: 131 }, // rgb(131, 214, 131)
|
|
||||||
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
|
|
||||||
{ r: 220, g: 144, b: 101 }, // rgb(220, 144, 101)
|
|
||||||
{ r: 224, g: 117, b: 117 }, // rgb(224, 117, 117)
|
|
||||||
{ r: 213, g: 139, b: 202 }, // rgb(213, 139, 202)
|
|
||||||
{ r: 161, g: 120, b: 214 }, // rgb(161, 120, 214)
|
|
||||||
];
|
|
||||||
|
|
||||||
const getRGMaskFill = (state: CanvasState): RgbColor => {
|
|
||||||
const lastFill = state.regions.entities.slice(-1)[0]?.fill.color;
|
|
||||||
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
|
|
||||||
if (i === -1) {
|
|
||||||
i = 0;
|
|
||||||
}
|
|
||||||
i = (i + 1) % DEFAULT_MASK_COLORS.length;
|
|
||||||
const fill = DEFAULT_MASK_COLORS[i];
|
|
||||||
assert(fill, 'This should never happen');
|
|
||||||
return fill;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const regionsReducers = {
|
|
||||||
rgAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }>
|
|
||||||
) => {
|
|
||||||
const { id, overrides, isSelected } = action.payload;
|
|
||||||
const entity: CanvasRegionalGuidanceState = {
|
|
||||||
id,
|
|
||||||
name: null,
|
|
||||||
type: 'regional_guidance',
|
|
||||||
isEnabled: true,
|
|
||||||
objects: [],
|
|
||||||
fill: {
|
|
||||||
style: 'solid',
|
|
||||||
color: getRGMaskFill(state),
|
|
||||||
},
|
|
||||||
opacity: 0.5,
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
autoNegative: true,
|
|
||||||
positivePrompt: '',
|
|
||||||
negativePrompt: null,
|
|
||||||
ipAdapters: [],
|
|
||||||
};
|
|
||||||
merge(entity, overrides);
|
|
||||||
state.regions.entities.push(entity);
|
|
||||||
if (isSelected) {
|
|
||||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare: (payload?: { overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }) => ({
|
|
||||||
payload: { ...payload, id: getPrefixedId('regional_guidance') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => {
|
|
||||||
const { data } = action.payload;
|
|
||||||
state.regions.entities.push(data);
|
|
||||||
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
|
|
||||||
},
|
|
||||||
rgPositivePromptChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ prompt: string | null }, 'regional_guidance'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, prompt } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.positivePrompt = prompt;
|
|
||||||
},
|
|
||||||
rgNegativePromptChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ prompt: string | null }, 'regional_guidance'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, prompt } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.negativePrompt = prompt;
|
|
||||||
},
|
|
||||||
rgFillColorChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'regional_guidance'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, color } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.fill.color = color;
|
|
||||||
},
|
|
||||||
rgFillStyleChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'regional_guidance'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, style } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.fill.style = style;
|
|
||||||
},
|
|
||||||
|
|
||||||
rgAutoNegativeToggled: (state, action: PayloadAction<EntityIdentifierPayload<void, 'regional_guidance'>>) => {
|
|
||||||
const { entityIdentifier } = action.payload;
|
|
||||||
const rg = selectEntity(state, entityIdentifier);
|
|
||||||
if (!rg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rg.autoNegative = !rg.autoNegative;
|
|
||||||
},
|
|
||||||
rgIPAdapterAdded: {
|
|
||||||
reducer: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
EntityIdentifierPayload<
|
|
||||||
{ ipAdapterId: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> },
|
|
||||||
'regional_guidance'
|
|
||||||
>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, overrides, ipAdapterId } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ipAdapter = { ...deepClone(initialIPAdapter), id: ipAdapterId };
|
|
||||||
merge(ipAdapter, overrides);
|
|
||||||
entity.ipAdapters.push(ipAdapter);
|
|
||||||
},
|
|
||||||
prepare: (
|
|
||||||
payload: EntityIdentifierPayload<{ overrides?: Partial<RegionalGuidanceIPAdapterConfig> }, 'regional_guidance'>
|
|
||||||
) => ({
|
|
||||||
payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
rgIPAdapterDeleted: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string }, 'regional_guidance'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, ipAdapterId } = action.payload;
|
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entity.ipAdapters = entity.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
|
|
||||||
},
|
|
||||||
rgIPAdapterImageChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
EntityIdentifierPayload<{ ipAdapterId: string; imageDTO: ImageDTO | null }, 'regional_guidance'>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, ipAdapterId, imageDTO } = action.payload;
|
|
||||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
|
||||||
if (!ipAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
|
||||||
},
|
|
||||||
rgIPAdapterWeightChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; weight: number }, 'regional_guidance'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, ipAdapterId, weight } = action.payload;
|
|
||||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
|
||||||
if (!ipAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ipAdapter.weight = weight;
|
|
||||||
},
|
|
||||||
rgIPAdapterBeginEndStepPctChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
EntityIdentifierPayload<{ ipAdapterId: string; beginEndStepPct: [number, number] }, 'regional_guidance'>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, ipAdapterId, beginEndStepPct } = action.payload;
|
|
||||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
|
||||||
if (!ipAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ipAdapter.beginEndStepPct = beginEndStepPct;
|
|
||||||
},
|
|
||||||
rgIPAdapterMethodChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; method: IPMethodV2 }, 'regional_guidance'>>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, ipAdapterId, method } = action.payload;
|
|
||||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
|
||||||
if (!ipAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ipAdapter.method = method;
|
|
||||||
},
|
|
||||||
rgIPAdapterModelChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
EntityIdentifierPayload<
|
|
||||||
{
|
|
||||||
ipAdapterId: string;
|
|
||||||
modelConfig: IPAdapterModelConfig | null;
|
|
||||||
},
|
|
||||||
'regional_guidance'
|
|
||||||
>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, ipAdapterId, modelConfig } = action.payload;
|
|
||||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
|
||||||
if (!ipAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
|
|
||||||
},
|
|
||||||
rgIPAdapterCLIPVisionModelChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<
|
|
||||||
EntityIdentifierPayload<{ ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'>
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const { entityIdentifier, ipAdapterId, clipVisionModel } = action.payload;
|
|
||||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
|
||||||
if (!ipAdapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ipAdapter.clipVisionModel = clipVisionModel;
|
|
||||||
},
|
|
||||||
} satisfies SliceCaseReducers<CanvasState>;
|
|
Loading…
x
Reference in New Issue
Block a user