refactor(ui): canvas v2 (wip)

This commit is contained in:
psychedelicious 2024-06-14 14:19:35 +10:00
parent 93b185dc3b
commit ca9090d070
54 changed files with 1759 additions and 1394 deletions

View File

@ -4,12 +4,12 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import type { AppDispatch } from 'app/store/store';
import { parseify } from 'common/util/serialize';
import {
caLayerImageChanged,
caLayerModelChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caLayerProcessorPendingBatchIdChanged,
caLayerRecalled,
controlAdapterImageChanged,
controlAdapterModelChanged,
controlAdapterProcessedImageChanged,
controlAdapterProcessorConfigChanged,
controlAdapterProcessorPendingBatchIdChanged,
controlAdapterRecalled,
} from 'features/controlLayers/store/controlLayersSlice';
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
@ -23,11 +23,11 @@ import { socketInvocationComplete } from 'services/events/actions';
import { assert } from 'tsafe';
const matcher = isAnyOf(
caLayerImageChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caLayerModelChanged,
caLayerRecalled
controlAdapterImageChanged,
controlAdapterProcessedImageChanged,
controlAdapterProcessorConfigChanged,
controlAdapterModelChanged,
controlAdapterRecalled
);
const DEBOUNCE_MS = 300;
@ -46,7 +46,7 @@ const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batc
} finally {
req.reset();
// Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null }));
}
};
@ -54,7 +54,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
startAppListening({
matcher,
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => {
const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId;
const layerId = controlAdapterRecalled.match(action) ? action.payload.id : action.payload.layerId;
const state = getState();
const originalState = getOriginalState();
@ -91,7 +91,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
// - If we have no image, we have nothing to process
// - If we have no processor config, we have nothing to process
// Clear the processed image and bail
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null }));
dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null }));
return;
}
@ -132,7 +132,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
const enqueueResult = await req.unwrap();
// TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional
assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue');
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id }));
dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id }));
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
// Wait for the processor node to complete
@ -155,8 +155,8 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
// Whew! We made it. Update the layer with the processed image
log.debug({ layerId, imageDTO }, 'ControlNet image processed');
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO }));
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO }));
dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null }));
} catch (error) {
if (signal.aborted) {
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
@ -174,7 +174,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
if (error instanceof Object) {
if ('data' in error && 'status' in error) {
if (error.status === 403) {
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
dispatch(controlAdapterImageChanged({ layerId, imageDTO: null }));
return;
}
}

View File

@ -8,11 +8,11 @@ import {
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
controlAdapterImageChanged,
iiLayerImageChanged,
imageAdded,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
layerImageAdded,
ipAdapterImageChanged,
regionalGuidanceIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
@ -99,7 +99,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
) {
const { layerId } = overData.context;
dispatch(
caLayerImageChanged({
controlAdapterImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
@ -117,7 +117,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
) {
const { layerId } = overData.context;
dispatch(
ipaLayerImageChanged({
ipAdapterImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
@ -135,7 +135,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
) {
const { layerId, ipAdapterId } = overData.context;
dispatch(
rgLayerIPAdapterImageChanged({
regionalGuidanceIPAdapterImageChanged({
layerId,
ipAdapterId,
imageDTO: activeData.payload.imageDTO,
@ -172,7 +172,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
) {
const { layerId } = overData.context;
dispatch(
imageAdded({
layerImageAdded({
layerId,
imageDTO: activeData.payload.imageDTO,
})

View File

@ -6,10 +6,10 @@ import {
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
controlAdapterImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
ipAdapterImageChanged,
regionalGuidanceIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
@ -122,7 +122,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(caLayerImageChanged({ layerId, imageDTO }));
dispatch(controlAdapterImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
@ -131,7 +131,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
dispatch(ipAdapterImageChanged({ layerId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
@ -140,7 +140,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
const { layerId, ipAdapterId } = postUploadAction;
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),

View File

@ -7,14 +7,16 @@ import type { JSONObject } from 'common/types';
import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import {
controlAdaptersPersistConfig,
controlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
controlAdaptersV2PersistConfig,
controlAdaptersV2Slice,
} from 'features/controlLayers/store/controlAdaptersSlice';
import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { ipAdaptersPersistConfig, ipAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice';
import { layersPersistConfig, layersSlice } from 'features/controlLayers/store/layersSlice';
import {
controlLayersPersistConfig,
controlLayersSlice,
controlLayersUndoableConfig,
} from 'features/controlLayers/store/controlLayersSlice';
regionalGuidancePersistConfig,
regionalGuidanceSlice,
} from 'features/controlLayers/store/regionalGuidanceSlice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
@ -49,6 +51,7 @@ import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware';
const allReducers = {
[api.reducerPath]: api.reducer,
[canvasSlice.name]: canvasSlice.reducer,
[gallerySlice.name]: gallerySlice.reducer,
[generationSlice.name]: generationSlice.reducer,
@ -56,7 +59,6 @@ const allReducers = {
[systemSlice.name]: systemSlice.reducer,
[configSlice.name]: configSlice.reducer,
[uiSlice.name]: uiSlice.reducer,
[controlAdaptersSlice.name]: controlAdaptersSlice.reducer,
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
[deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
@ -66,11 +68,14 @@ const allReducers = {
[queueSlice.name]: queueSlice.reducer,
[workflowSlice.name]: workflowSlice.reducer,
[hrfSlice.name]: hrfSlice.reducer,
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
[canvasV2Slice.name]: canvasV2Slice.reducer,
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[api.reducerPath]: api.reducer,
[upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.reducer,
[layersSlice.name]: layersSlice.reducer,
[controlAdaptersV2Slice.name]: controlAdaptersV2Slice.reducer,
[ipAdaptersSlice.name]: ipAdaptersSlice.reducer,
[regionalGuidanceSlice.name]: regionalGuidanceSlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@ -107,16 +112,19 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[systemPersistConfig.name]: systemPersistConfig,
[workflowPersistConfig.name]: workflowPersistConfig,
[uiPersistConfig.name]: uiPersistConfig,
[controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig,
[dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig,
[sdxlPersistConfig.name]: sdxlPersistConfig,
[loraPersistConfig.name]: loraPersistConfig,
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
[hrfPersistConfig.name]: hrfPersistConfig,
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
[canvasV2PersistConfig.name]: canvasV2PersistConfig,
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
[layersPersistConfig.name]: layersPersistConfig,
[controlAdaptersV2PersistConfig.name]: controlAdaptersV2PersistConfig,
[ipAdaptersPersistConfig.name]: ipAdaptersPersistConfig,
[regionalGuidancePersistConfig.name]: regionalGuidancePersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {

View File

@ -6,8 +6,8 @@ import {
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import type { LayerData } from 'features/controlLayers/store/types';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
@ -24,7 +24,7 @@ import { forEach, upperFirst } from 'lodash-es';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
const LAYER_TYPE_TO_TKEY: Record<Layer['type'], string> = {
const LAYER_TYPE_TO_TKEY: Record<LayerData['type'], string> = {
initial_image_layer: 'controlLayers.globalInitialImage',
control_adapter_layer: 'controlLayers.globalControlAdapter',
ip_adapter_layer: 'controlLayers.globalIPAdapter',
@ -41,7 +41,7 @@ const createSelector = (templates: Templates) =>
selectNodesSlice,
selectWorkflowSettingsSlice,
selectDynamicPromptsSlice,
selectControlLayersSlice,
selectCanvasV2Slice,
activeTabNameSelector,
selectUpscalelice,
selectConfigSlice,

View File

@ -1,85 +1,170 @@
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { describe, expect, it } from 'vitest';
describe('Array Manipulation Functions', () => {
const originalArray = ['a', 'b', 'c', 'd'];
describe('moveForwardOne', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
describe('moveOneToEnd', () => {
describe('with callback', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveForward(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item forward by one position', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the end', () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveToFront', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
describe('moveToStart', () => {
describe('with callback', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item to the front', () => {
const array = [...originalArray];
const result = moveToStart(array, 'c');
expect(result).toEqual(['c', 'a', 'b', 'd']);
});
it('should do nothing if the item is already at the front', () => {
const array = [...originalArray];
const result = moveToStart(array, 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToStart(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveBackwardsOne', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
describe('moveOneToStart', () => {
describe('with callback', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item backward by one position', () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'c');
expect(result).toEqual(['a', 'c', 'b', 'd']);
});
it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveOneToStart(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
describe('moveToBack', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
describe('moveToEnd', () => {
describe('with callback', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray);
});
});
describe('with item', () => {
it('should move an item to the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, 'b');
expect(result).toEqual(['a', 'c', 'd', 'b']);
});
it('should do nothing if the item is already at the back', () => {
const array = [...originalArray];
const result = moveToEnd(array, 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']);
});
it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray];
const result = moveToEnd(array, 'z');
expect(result).toEqual(originalArray);
});
});
});
});

View File

@ -1,37 +1,45 @@
export const moveForward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
if (index >= 0 && index < array.length - 1) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index + 1]] = [array[index + 1], array[index]];
}
return array;
};
export const moveToFront = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveToStart<T>(array: T[], item: T): T[];
export function moveToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index > 0) {
const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check
array.unshift(item);
}
return array;
};
}
export const moveBackward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveOneToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveOneToStart<T>(array: T[], item: T): T[];
export function moveOneToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index > 0) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index - 1]] = [array[index - 1], array[index]];
}
return array;
};
}
export const moveToBack = <T>(array: T[], callback: (item: T) => boolean): T[] => {
const index = array.findIndex(callback);
export function moveToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveToEnd<T>(array: T[], item: T): T[];
export function moveToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index >= 0 && index < array.length - 1) {
const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check
array.push(item);
}
return array;
};
}
export function moveOneToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
export function moveOneToEnd<T>(array: T[], item: T): T[];
export function moveOneToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
if (index >= 0 && index < array.length - 1) {
//@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index + 1]] = [array[index + 1], array[index]];
}
return array;
}

View File

@ -1,7 +1,7 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { rasterLayerAdded, rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { layerAdded, regionalGuidanceAdded } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
@ -13,10 +13,10 @@ export const AddLayerButton = memo(() => {
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
const [addIILayer, isAddIILayerDisabled] = useAddIILayer();
const addRGLayer = useCallback(() => {
dispatch(rgLayerAdded());
dispatch(regionalGuidanceAdded());
}, [dispatch]);
const addRasterLayer = useCallback(() => {
dispatch(rasterLayerAdded());
dispatch(layerAdded());
}, [dispatch]);
return (

View File

@ -3,9 +3,9 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
rgLayerNegativePromptChanged,
rgLayerPositivePromptChanged,
selectControlLayersSlice,
regionalGuidanceNegativePromptChanged,
regionalGuidancePositivePromptChanged,
selectCanvasV2Slice,
} from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { useCallback, useMemo } from 'react';
@ -22,7 +22,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId);
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return {
@ -34,10 +34,10 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' }));
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
const addNegativePrompt = useCallback(() => {
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
return (

View File

@ -1,13 +1,13 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter';
import {
caLayerControlModeChanged,
caLayerImageChanged,
caLayerModelChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caOrIPALayerBeginEndStepPctChanged,
caOrIPALayerWeightChanged,
controlAdapterControlModeChanged,
controlAdapterImageChanged,
controlAdapterModelChanged,
controlAdapterProcessedImageChanged,
controlAdapterProcessorConfigChanged,
selectLayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
@ -46,7 +46,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeControlMode = useCallback(
(controlMode: ControlModeV2) => {
dispatch(
caLayerControlModeChanged({
controlAdapterControlModeChanged({
layerId,
controlMode,
})
@ -64,7 +64,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeProcessorConfig = useCallback(
(processorConfig: ProcessorConfig | null) => {
dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig }));
dispatch(controlAdapterProcessorConfigChanged({ layerId, processorConfig }));
},
[dispatch, layerId]
);
@ -72,7 +72,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
dispatch(
caLayerModelChanged({
controlAdapterModelChanged({
layerId,
modelConfig,
})
@ -83,17 +83,17 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(caLayerImageChanged({ layerId, imageDTO }));
dispatch(controlAdapterImageChanged({ layerId, imageDTO }));
},
[dispatch, layerId]
);
const onErrorLoadingImage = useCallback(() => {
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
dispatch(controlAdapterImageChanged({ layerId, imageDTO: null }));
}, [dispatch, layerId]);
const onErrorLoadingProcessedImage = useCallback(() => {
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null }));
dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null }));
}, [dispatch, layerId]);
const droppableData = useMemo<CALayerImageDropData>(

View File

@ -11,14 +11,14 @@ import { IILayer } from 'features/controlLayers/components/IILayer/IILayer';
import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer';
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import type { LayerData } from 'features/controlLayers/store/types';
import { isRenderableLayer } from 'features/controlLayers/store/types';
import { partition } from 'lodash-es';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer);
return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse();
});
@ -50,7 +50,7 @@ ControlLayersPanelContent.displayName = 'ControlLayersPanelContent';
type LayerWrapperProps = {
id: string;
type: Layer['type'];
type: LayerData['type'];
};
const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {

View File

@ -3,10 +3,10 @@ import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter
import {
caOrIPALayerBeginEndStepPctChanged,
caOrIPALayerWeightChanged,
ipaLayerCLIPVisionModelChanged,
ipaLayerImageChanged,
ipaLayerMethodChanged,
ipaLayerModelChanged,
ipAdapterCLIPVisionModelChanged,
ipAdapterImageChanged,
ipAdapterMethodChanged,
ipAdapterModelChanged,
selectLayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import { isIPAdapterLayer } from 'features/controlLayers/store/types';
@ -46,28 +46,28 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
dispatch(ipaLayerMethodChanged({ layerId, method }));
dispatch(ipAdapterMethodChanged({ layerId, method }));
},
[dispatch, layerId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
dispatch(ipaLayerModelChanged({ layerId, modelConfig }));
dispatch(ipAdapterModelChanged({ layerId, modelConfig }));
},
[dispatch, layerId]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel }));
dispatch(ipAdapterCLIPVisionModelChanged({ layerId, clipVisionModel }));
},
[dispatch, layerId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
dispatch(ipAdapterImageChanged({ layerId, imageDTO }));
},
[dispatch, layerId]
);

View File

@ -6,7 +6,7 @@ import {
layerMovedForward,
layerMovedToBack,
layerMovedToFront,
selectControlLayersSlice,
selectCanvasV2Slice,
} from 'features/controlLayers/store/controlLayersSlice';
import { isRenderableLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
@ -21,7 +21,7 @@ export const LayerMenuArrangeActions = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`);
const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId);

View File

@ -3,9 +3,9 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
rgLayerNegativePromptChanged,
rgLayerPositivePromptChanged,
selectControlLayersSlice,
regionalGuidanceNegativePromptChanged,
regionalGuidancePositivePromptChanged,
selectCanvasV2Slice,
} from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
@ -21,7 +21,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId);
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return {
@ -33,10 +33,10 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => {
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' }));
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
const addNegativePrompt = useCallback(() => {
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
return (
<>

View File

@ -16,7 +16,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import {
layerOpacityChanged,
selectControlLayersSlice,
selectCanvasV2Slice,
selectLayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import { isLayerWithOpacity } from 'features/controlLayers/store/types';
@ -36,7 +36,7 @@ export const LayerOpacity = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const selectOpacity = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity);
return Math.round(layer.opacity * 100);
}),

View File

@ -1,10 +1,10 @@
import { Text } from '@invoke-ai/ui-library';
import type { Layer } from 'features/controlLayers/store/types';
import type { LayerData } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
type: Layer['type'];
type: LayerData['type'];
};
export const LayerTitle = memo(({ type }: Props) => {

View File

@ -8,7 +8,7 @@ import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMe
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -29,7 +29,7 @@ export const RGLayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return {

View File

@ -1,7 +1,7 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgLayerAutoNegativeChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { regionalGuidanceAutoNegativeChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
@ -15,7 +15,7 @@ type Props = {
const useAutoNegative = (layerId: string) => {
const selectAutoNegative = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return layer.autoNegative;
@ -32,7 +32,7 @@ export const RGLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => {
const autoNegative = useAutoNegative(layerId);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(rgLayerAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' }));
dispatch(regionalGuidanceAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' }));
},
[dispatch, layerId]
);

View File

@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import { stopPropagation } from 'common/util/stopPropagation';
import { rgbColorToString } from 'features/canvas/util/colorToString';
import { rgLayerPreviewColorChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { rgFillChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import type { RgbColor } from 'react-colorful';
@ -19,7 +19,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const selectColor = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`);
return layer.previewColor;
@ -30,7 +30,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const onColorChange = useCallback(
(color: RgbColor) => {
dispatch(rgLayerPreviewColorChanged({ layerId, color }));
dispatch(rgFillChanged({ layerId, color }));
},
[dispatch, layerId]
);

View File

@ -2,7 +2,7 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
import { assert } from 'tsafe';
@ -14,7 +14,7 @@ type Props = {
export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
const selectIPAdapterIds = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return layer.ipAdapters;

View File

@ -2,13 +2,13 @@ import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter';
import {
rgLayerIPAdapterBeginEndStepPctChanged,
rgLayerIPAdapterCLIPVisionModelChanged,
rgLayerIPAdapterDeleted,
rgLayerIPAdapterImageChanged,
rgLayerIPAdapterMethodChanged,
rgLayerIPAdapterModelChanged,
rgLayerIPAdapterWeightChanged,
regionalGuidanceIPAdapterBeginEndStepPctChanged,
regionalGuidanceIPAdapterCLIPVisionModelChanged,
regionalGuidanceIPAdapterDeleted,
regionalGuidanceIPAdapterImageChanged,
regionalGuidanceIPAdapterMethodChanged,
regionalGuidanceIPAdapterModelChanged,
regionalGuidanceIPAdapterWeightChanged,
selectRGLayerIPAdapterOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
@ -26,14 +26,14 @@ type Props = {
export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => {
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId }));
dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId }));
}, [dispatch, ipAdapterId, layerId]);
const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId));
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(
rgLayerIPAdapterBeginEndStepPctChanged({
regionalGuidanceIPAdapterBeginEndStepPctChanged({
layerId,
ipAdapterId,
beginEndStepPct,
@ -45,35 +45,35 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(rgLayerIPAdapterWeightChanged({ layerId, ipAdapterId, weight }));
dispatch(regionalGuidanceIPAdapterWeightChanged({ layerId, ipAdapterId, weight }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method }));
dispatch(regionalGuidanceIPAdapterMethodChanged({ layerId, ipAdapterId, method }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
dispatch(rgLayerIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig }));
dispatch(regionalGuidanceIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel }));
dispatch(regionalGuidanceIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
},
[dispatch, ipAdapterId, layerId]
);

View File

@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton';
import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks';
import { rgLayerNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { regionalGuidanceNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -21,7 +21,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: v }));
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: v }));
},
[dispatch, layerId]
);

View File

@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton';
import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks';
import { rgLayerPositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { regionalGuidancePositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -21,7 +21,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
dispatch(rgLayerPositivePromptChanged({ layerId, prompt: v }));
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: v }));
},
[dispatch, layerId]
);

View File

@ -1,8 +1,8 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import {
rgLayerNegativePromptChanged,
rgLayerPositivePromptChanged,
regionalGuidanceNegativePromptChanged,
regionalGuidancePositivePromptChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -18,9 +18,9 @@ export const RGLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) =>
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
if (polarity === 'positive') {
dispatch(rgLayerPositivePromptChanged({ layerId, prompt: null }));
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: null }));
} else {
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: null }));
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: null }));
}
}, [dispatch, layerId, polarity]);
return (

View File

@ -2,8 +2,8 @@ import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-li
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
import {
BRUSH_SPACING_PCT,
@ -15,16 +15,16 @@ import { setStageEventHandlers } from 'features/controlLayers/konva/events';
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
import {
$bbox,
$brushColor,
$brushSize,
$brushSpacingPx,
$brushWidth,
$fill,
$invertScroll,
$isDrawing,
$isMouseDown,
$lastAddedPoint,
$lastCursorPos,
$lastMouseDownPos,
$selectedLayer,
$shouldInvertBrushSizeScrollDirection,
$spaceKey,
$stageAttrs,
$tool,
@ -37,15 +37,16 @@ import {
layerTranslated,
linePointsAdded,
rectAdded,
selectControlLayersSlice,
selectCanvasV2Slice,
} from 'features/controlLayers/store/controlLayersSlice';
import { selectLayersSlice } from 'features/controlLayers/store/layersSlice';
import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice';
import type {
AddBrushLineArg,
AddEraserLineArg,
AddPointToLineArg,
AddRectShapeArg,
} from 'features/controlLayers/store/types';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import { clamp } from 'lodash-es';
@ -60,26 +61,26 @@ Konva.showWarnings = false;
const log = logger('controlLayers');
const selectBrushColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers
.filter(isRegionalGuidanceLayer)
.find((l) => l.id === controlLayers.present.selectedLayerId);
const selectBrushColor = createSelector(
selectCanvasV2Slice,
selectLayersSlice,
selectRegionalGuidanceSlice,
(canvas, layers, regionalGuidance) => {
const rg = regionalGuidance.regions.find((i) => i.id === canvas.lastSelectedItem?.id);
if (layer) {
return { ...layer.previewColor, a: controlLayers.present.globalMaskLayerOpacity };
if (rg) {
return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity });
}
return rgbaColorToString(canvas.tool.fill);
}
);
return controlLayers.present.brushColor;
});
const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLayers) => {
const selectSelectedLayer = createSelector(selectCanvasV2Slice, (controlLayers) => {
return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null;
});
const selectLayerCount = createSelector(
selectControlLayersSlice,
(controlLayers) => controlLayers.present.layers.length
);
const selectLayerCount = createSelector(selectCanvasV2Slice, (controlLayers) => controlLayers.present.layers.length);
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
const dispatch = useAppDispatch();
@ -100,11 +101,11 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
);
useLayoutEffect(() => {
$brushColor.set(brushColor);
$brushSize.set(state.brushSize);
$fill.set(brushColor);
$brushWidth.set(state.brushSize);
$brushSpacingPx.set(brushSpacingPx);
$selectedLayer.set(selectedLayer);
$shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection);
$invertScroll.set(shouldInvertBrushSizeScrollDirection);
$bbox.set(state.bbox);
}, [
brushSpacingPx,
@ -196,8 +197,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
setIsDrawing: $isDrawing.set,
getIsMouseDown: $isMouseDown.get,
setIsMouseDown: $isMouseDown.set,
getBrushColor: $brushColor.get,
getBrushSize: $brushSize.get,
getBrushColor: $fill.get,
getBrushSize: $brushWidth.get,
getBrushSpacingPx: $brushSpacingPx.get,
getSelectedLayer: $selectedLayer.get,
getLastAddedPoint: $lastAddedPoint.get,
@ -206,7 +207,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
setLastCursorPos: $lastCursorPos.set,
getLastMouseDownPos: $lastMouseDownPos.get,
setLastMouseDownPos: $lastMouseDownPos.set,
getShouldInvert: $shouldInvertBrushSizeScrollDirection.get,
getShouldInvert: $invertScroll.get,
getSpaceKey: $spaceKey.get,
setStageAttrs: $stageAttrs.set,
onBrushSizeChanged,

View File

@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$tool,
layerReset,
selectControlLayersSlice,
selectCanvasV2Slice,
selectedLayerDeleted,
} from 'features/controlLayers/store/controlLayersSlice';
import { useCallback } from 'react';
@ -20,7 +20,7 @@ import {
PiRectangleBold,
} from 'react-icons/pi';
const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => {
const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => {
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer';
});

View File

@ -1,9 +1,9 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
caLayerAdded,
controlAdapterAdded,
iiLayerAdded,
ipaLayerAdded,
rgLayerIPAdapterAdded,
ipAdapterAdded,
regionalGuidanceIPAdapterAdded,
} from 'features/controlLayers/store/controlLayersSlice';
import { isInitialImageLayer } from 'features/controlLayers/store/types';
import {
@ -46,7 +46,7 @@ export const useAddCALayer = () => {
processorConfig,
});
dispatch(caLayerAdded(controlAdapter));
dispatch(controlAdapterAdded(controlAdapter));
}, [dispatch, model, baseModel]);
return [addCALayer, isDisabled] as const;
@ -70,7 +70,7 @@ export const useAddIPALayer = () => {
const ipAdapter = buildIPAdapter(id, {
model: zModelIdentifierField.parse(model),
});
dispatch(ipaLayerAdded(ipAdapter));
dispatch(ipAdapterAdded(ipAdapter));
}, [dispatch, model]);
return [addIPALayer, isDisabled] as const;
@ -94,7 +94,7 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => {
const ipAdapter = buildIPAdapter(id, {
model: zModelIdentifierField.parse(model),
});
dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapter }));
dispatch(regionalGuidanceIPAdapterAdded({ layerId, ipAdapter }));
}, [dispatch, model, layerId]);
return [addIPAdapter, isDisabled] as const;

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { assert } from 'tsafe';
@ -9,7 +9,7 @@ import { assert } from 'tsafe';
export const useLayerPositivePrompt = (layerId: string) => {
const selectLayer = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`);
@ -24,7 +24,7 @@ export const useLayerPositivePrompt = (layerId: string) => {
export const useLayerNegativePrompt = (layerId: string) => {
const selectLayer = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`);
@ -39,7 +39,7 @@ export const useLayerNegativePrompt = (layerId: string) => {
export const useLayerIsEnabled = (layerId: string) => {
const selectLayer = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return layer.isEnabled;
@ -53,7 +53,7 @@ export const useLayerIsEnabled = (layerId: string) => {
export const useLayerType = (layerId: string) => {
const selectLayer = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return layer.type;
@ -67,7 +67,7 @@ export const useLayerType = (layerId: string) => {
export const useCALayerOpacity = (layerId: string) => {
const selectLayer = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled };

View File

@ -6,7 +6,7 @@ import type {
AddEraserLineArg,
AddPointToLineArg,
AddRectShapeArg,
Layer,
LayerData,
StageAttrs,
Tool,
} from 'features/controlLayers/store/types';
@ -38,7 +38,7 @@ type Arg = {
getBrushColor: () => RgbaColor;
getBrushSize: () => number;
getBrushSpacingPx: () => number;
getSelectedLayer: () => Layer | null;
getSelectedLayer: () => LayerData | null;
getShouldInvert: () => boolean;
getSpaceKey: () => boolean;
onBrushLineAdded: (arg: AddBrushLineArg) => void;
@ -72,7 +72,7 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC
* @param onPointAddedToLine The callback to add a point to a line
*/
const maybeAddNextPoint = (
layerId: string,
selectedLayer: LayerData,
currentPos: Vector2d,
getLastAddedPoint: Arg['getLastAddedPoint'],
setLastAddedPoint: Arg['setLastAddedPoint'],
@ -88,7 +88,7 @@ const maybeAddNextPoint = (
}
}
setLastAddedPoint(currentPos);
onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] });
onPointAddedToLine({ layerId, point: [currentPos.x - selectedLayer.x, currentPos.y - selectedLayer.y] });
};
export const setStageEventHandlers = ({
@ -158,7 +158,7 @@ export const setStageEventHandlers = ({
if (tool === 'brush') {
onBrushLineAdded({
layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y],
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
});
}
@ -166,7 +166,7 @@ export const setStageEventHandlers = ({
if (tool === 'eraser') {
onEraserLineAdded({
layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y],
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
});
}
@ -262,7 +262,7 @@ export const setStageEventHandlers = ({
// Start a new line
onBrushLineAdded({
layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y],
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
});
setIsDrawing(true);
@ -282,7 +282,10 @@ export const setStageEventHandlers = ({
);
} else {
// Start a new line
onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y] });
onEraserLineAdded({
layerId: selectedLayer.id,
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
});
setIsDrawing(true);
}
}

View File

@ -41,6 +41,8 @@ export const RASTER_LAYER_ERASER_LINE_NAME = 'raster_layer.eraser_line';
export const RASTER_LAYER_RECT_SHAPE_NAME = 'raster_layer.rect_shape';
export const RASTER_LAYER_IMAGE_NAME = 'raster_layer.image';
export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer';
// Getters for non-singleton layer and object IDs
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`;

View File

@ -6,8 +6,8 @@ import {
RG_LAYER_OBJECT_GROUP_NAME,
} from 'features/controlLayers/konva/naming';
import { createBboxRect } from 'features/controlLayers/konva/renderers/objects';
import type { Layer } from 'features/controlLayers/store/types';
import { isRegionalGuidanceLayer, isRGOrRasterlayer } from 'features/controlLayers/store/types';
import type { LayerData } from 'features/controlLayers/store/types';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import { assert } from 'tsafe';
@ -185,10 +185,10 @@ const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RAST
*/
export const updateBboxes = (
stage: Konva.Stage,
layerStates: Layer[],
layerStates: LayerData[],
onBboxChanged: (layerId: string, bbox: IRect | null) => void
): void => {
for (const layerState of layerStates.filter(isRGOrRasterlayer)) {
for (const layerState of layerStates) {
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`);
assert(konvaLayer, `Layer ${layerState.id} not found in stage`);
// We only need to recalculate the bbox if the layer has changed

View File

@ -7,10 +7,11 @@ import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/kon
import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util';
import type { Layer, Tool } from 'features/controlLayers/store/types';
import type { LayerData, Tool } from 'features/controlLayers/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
isInpaintMaskLayer,
isRasterLayer,
isRegionalGuidanceLayer,
isRenderableLayer,
@ -34,7 +35,7 @@ import type { ImageDTO } from 'services/api/types';
*/
const renderLayers = (
stage: Konva.Stage,
layerStates: Layer[],
layerStates: LayerData[],
globalMaskLayerOpacity: number,
tool: Tool,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>,
@ -52,15 +53,14 @@ const renderLayers = (
for (const layer of layerStates) {
if (isRegionalGuidanceLayer(layer)) {
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged);
}
if (isControlAdapterLayer(layer)) {
} else if (isControlAdapterLayer(layer)) {
renderCALayer(stage, layer, zIndex, getImageDTO);
}
if (isInitialImageLayer(layer)) {
} else if (isInitialImageLayer(layer)) {
renderIILayer(stage, layer, zIndex, getImageDTO);
}
if (isRasterLayer(layer)) {
} else if (isRasterLayer(layer)) {
renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged);
} else if (isInpaintMaskLayer(layer)) {
//
}
// IP Adapter layers are not rendered
// Increment the z-index for the tool layer

View File

@ -5,7 +5,7 @@ import {
LAYER_BBOX_NAME,
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
} from 'features/controlLayers/konva/naming';
import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types';
import type { BrushLine, EraserLine, ImageObject, LayerData, RectShape } from 'features/controlLayers/store/types';
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import { t } from 'i18next';
import Konva from 'konva';
@ -177,7 +177,7 @@ export const createImageObjectGroup = async (
* @param layerState The layer state for the layer to create the bounding box for
* @param konvaLayer The konva layer to attach the bounding box to
*/
export const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => {
export const createBboxRect = (layerState: LayerData, konvaLayer: Konva.Layer): Konva.Rect => {
const rect = new Konva.Rect({
id: getLayerBboxId(layerState.id),
name: LAYER_BBOX_NAME,

View File

@ -18,7 +18,7 @@ import {
PREVIEW_TOOL_GROUP_ID,
} from 'features/controlLayers/konva/naming';
import { selectRenderableLayers } from 'features/controlLayers/konva/util';
import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types';
import type { LayerData, RgbaColor, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types';
import { atom } from 'nanostores';
@ -338,7 +338,7 @@ export const renderToolPreview = (
stage: Konva.Stage,
tool: Tool,
brushColor: RgbaColor,
selectedLayerType: Layer['type'] | null,
selectedLayerType: LayerData['type'] | null,
globalMaskLayerOpacity: number,
cursorPos: Vector2d | null,
lastMouseDownPos: Vector2d | null,

View File

@ -1,6 +1,7 @@
import {
CA_LAYER_NAME,
INITIAL_IMAGE_LAYER_NAME,
INPAINT_MASK_LAYER_NAME,
RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_IMAGE_NAME,
@ -98,7 +99,8 @@ export const selectRenderableLayers = (node: Konva.Node): boolean =>
node.name() === RG_LAYER_NAME ||
node.name() === CA_LAYER_NAME ||
node.name() === INITIAL_IMAGE_LAYER_NAME ||
node.name() === RASTER_LAYER_NAME;
node.name() === RASTER_LAYER_NAME ||
node.name() === INPAINT_MASK_LAYER_NAME;
/**
* Konva selection callback to select RG mask objects. This includes lines and rects.

View File

@ -0,0 +1,282 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { v4 as uuidv4 } from 'uuid';
import type { ControlAdapterConfig, ControlAdapterData, Filter } from './types';
type ControlAdaptersV2State = {
_version: 1;
controlAdapters: ControlAdapterData[];
};
const initialState: ControlAdaptersV2State = {
_version: 1,
controlAdapters: [],
};
const selectCa = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id);
export const controlAdaptersV2Slice = createSlice({
name: 'controlAdaptersV2',
initialState,
reducers: {
caAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => {
const { id, config } = action.payload;
state.controlAdapters.push({
id,
type: 'control_adapter',
x: 0,
y: 0,
bbox: null,
bboxNeedsUpdate: false,
isEnabled: true,
opacity: 1,
filter: 'lightness_to_alpha',
processorPendingBatchId: null,
...config,
});
},
prepare: (config: ControlAdapterConfig) => ({
payload: { id: uuidv4(), config },
}),
},
caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => {
state.controlAdapters.push(action.payload.data);
},
caIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => {
const { id, isEnabled } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.isEnabled = isEnabled;
},
caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.x = x;
ca.y = y;
},
caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.bbox = bbox;
ca.bboxNeedsUpdate = false;
},
caDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id);
},
caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.opacity = opacity;
},
caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
moveOneToEnd(state.controlAdapters, ca);
},
caMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
moveToEnd(state.controlAdapters, ca);
},
caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
moveOneToStart(state.controlAdapters, ca);
},
caMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
moveToStart(state.controlAdapters, ca);
},
caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
if (imageDTO) {
const newImage = imageDTOToImageWithDims(imageDTO);
if (isEqual(newImage, ca.image)) {
return;
}
ca.image = newImage;
ca.processedImage = null;
} else {
ca.image = null;
ca.processedImage = null;
}
},
caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
caModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
}>
) => {
const { id, modelConfig } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
if (!modelConfig) {
ca.model = null;
return;
}
ca.model = zModelIdentifierField.parse(modelConfig);
// We may need to convert the CA to match the model
if (!ca.controlMode && ca.model.type === 'controlnet') {
ca.controlMode = 'balanced';
} else if (ca.controlMode && ca.model.type === 't2i_adapter') {
ca.controlMode = null;
}
const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig);
if (candidateProcessorConfig?.type !== ca.processorConfig?.type) {
// The processor has changed. For example, the previous model was a Canny model and the new model is a Depth
// model. We need to use the new processor.
ca.processedImage = null;
ca.processorConfig = candidateProcessorConfig;
}
},
caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {
const { id, controlMode } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.controlMode = controlMode;
},
caProcessorConfigChanged: (
state,
action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }>
) => {
const { id, processorConfig } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.processorConfig = processorConfig;
if (!processorConfig) {
ca.processedImage = null;
}
},
caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => {
const { id, filter } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.filter = filter;
},
caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => {
const { id, batchId } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.processorPendingBatchId = batchId;
},
caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.weight = weight;
},
caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const ca = selectCa(state, id);
if (!ca) {
return;
}
ca.beginEndStepPct = beginEndStepPct;
},
},
});
export const {
caAdded,
caBboxChanged,
caDeleted,
caIsEnabledChanged,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
caOpacityChanged,
caTranslated,
caRecalled,
caImageChanged,
caProcessedImageChanged,
caModelChanged,
caControlModeChanged,
caProcessorConfigChanged,
caFilterChanged,
caProcessorPendingBatchIdChanged,
caWeightChanged,
caBeginEndStepPctChanged,
} = controlAdaptersV2Slice.actions;
export const selectControlAdaptersV2Slice = (state: RootState) => state.controlAdaptersV2;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const controlAdaptersV2PersistConfig: PersistConfig<ControlAdaptersV2State> = {
name: controlAdaptersV2Slice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -0,0 +1,140 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { v4 as uuidv4 } from 'uuid';
import type { IPAdapterConfig, IPAdapterData } from './types';
type IPAdaptersState = {
_version: 1;
ipAdapters: IPAdapterData[];
};
const initialState: IPAdaptersState = {
_version: 1,
ipAdapters: [],
};
const selectIpa = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id);
export const ipAdaptersSlice = createSlice({
name: 'ipAdapters',
initialState,
reducers: {
ipaAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => {
const { id, config } = action.payload;
const layer: IPAdapterData = {
id,
type: 'ip_adapter',
isEnabled: true,
...config,
};
state.ipAdapters.push(layer);
},
prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }),
},
ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => {
state.ipAdapters.push(action.payload.data);
},
ipaIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => {
const { id, isEnabled } = action.payload;
const ipa = selectIpa(state, id);
if (ipa) {
ipa.isEnabled = isEnabled;
}
},
ipaDeleted: (state, action: PayloadAction<{ id: string }>) => {
state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id);
},
ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ipa = selectIpa(state, id);
if (!ipa) {
return;
}
ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => {
const { id, method } = action.payload;
const ipa = selectIpa(state, id);
if (!ipa) {
return;
}
ipa.method = method;
},
ipaModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: IPAdapterModelConfig | null;
}>
) => {
const { id, modelConfig } = action.payload;
const ipa = selectIpa(state, id);
if (!ipa) {
return;
}
if (modelConfig) {
ipa.model = zModelIdentifierField.parse(modelConfig);
} else {
ipa.model = null;
}
},
ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => {
const { id, clipVisionModel } = action.payload;
const ipa = selectIpa(state, id);
if (!ipa) {
return;
}
ipa.clipVisionModel = clipVisionModel;
},
ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const ipa = selectIpa(state, id);
if (!ipa) {
return;
}
ipa.weight = weight;
},
ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const ipa = selectIpa(state, id);
if (!ipa) {
return;
}
ipa.beginEndStepPct = beginEndStepPct;
},
},
});
export const {
ipaAdded,
ipaRecalled,
ipaIsEnabledChanged,
ipaDeleted,
ipaImageChanged,
ipaMethodChanged,
ipaModelChanged,
ipaCLIPVisionModelChanged,
ipaWeightChanged,
ipaBeginEndStepPctChanged,
} = ipAdaptersSlice.actions;
export const selectIPAdaptersSlice = (state: RootState) => state.ipAdapters;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const ipAdaptersPersistConfig: PersistConfig<IPAdaptersState> = {
name: ipAdaptersSlice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -0,0 +1,268 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { IRect } from 'konva/lib/types';
import { v4 as uuidv4 } from 'uuid';
import type {
AddBrushLineArg,
AddEraserLineArg,
AddImageObjectArg,
AddPointToLineArg,
AddRectShapeArg,
LayerData,
} from './types';
import { isLine } from './types';
type LayersState = {
_version: 1;
layers: LayerData[];
};
const initialState: LayersState = { _version: 1, layers: [] };
const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id);
export const layersSlice = createSlice({
name: 'layers',
initialState,
reducers: {
layerAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.layers.push({
id,
type: 'layer',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
objects: [],
opacity: 1,
x: 0,
y: 0,
});
},
prepare: () => ({ payload: { id: uuidv4() } }),
},
layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => {
state.layers.push(action.payload.data);
},
layerIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => {
const { id, isEnabled } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = isEnabled;
},
layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.x = x;
layer.y = y;
},
layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.bbox = bbox;
layer.bboxNeedsUpdate = false;
if (bbox === null) {
layer.objects = [];
}
},
layerReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = true;
layer.objects = [];
layer.bbox = null;
layer.bboxNeedsUpdate = false;
},
layerDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.layers = state.layers.filter((l) => l.id !== id);
},
layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.opacity = opacity;
},
layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToEnd(state.layers, layer);
},
layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToEnd(state.layers, layer);
},
layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToStart(state.layers, layer);
},
layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToStart(state.layers, layer);
},
layerBrushLineAdded: {
reducer: (state, action: PayloadAction<AddBrushLineArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
id: getBrushLineId(id, lineId),
type: 'brush_line',
points,
strokeWidth: width,
color,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: AddBrushLineArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerEraserLineAdded: {
reducer: (state, action: PayloadAction<AddEraserLineArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
id: getEraserLineId(id, lineId),
type: 'eraser_line',
points,
strokeWidth: width,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: AddEraserLineArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerLinePointAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
const { id, point } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const lastObject = layer.objects[layer.objects.length - 1];
if (!lastObject || !isLine(lastObject)) {
return;
}
lastObject.points.push(...point);
layer.bboxNeedsUpdate = true;
},
layerRectAdded: {
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
return;
}
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
x: rect.x - layer.x,
y: rect.y - layer.y,
width: rect.width,
height: rect.height,
color,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
},
layerImageAdded: {
reducer: (state, action: PayloadAction<AddImageObjectArg & { imageId: string }>) => {
const { id, imageId, imageDTO } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const { width, height, image_name: name } = imageDTO;
layer.objects.push({
type: 'image',
id: getImageObjectId(id, imageId),
x: 0,
y: 0,
width,
height,
image: { width, height, name },
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageId: uuidv4() } }),
},
},
});
export const {
layerAdded,
layerDeleted,
layerReset,
layerMovedForwardOne,
layerMovedToFront,
layerMovedBackwardOne,
layerMovedToBack,
layerIsEnabledChanged,
layerOpacityChanged,
layerTranslated,
layerBboxChanged,
layerBrushLineAdded,
layerEraserLineAdded,
layerLinePointAdded,
layerRectAdded,
layerImageAdded,
} = layersSlice.actions;
export const selectLayersSlice = (state: RootState) => state.layers;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const layersPersistConfig: PersistConfig<LayersState> = {
name: layersSlice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -0,0 +1,440 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
AddBrushLineArg,
AddEraserLineArg,
AddPointToLineArg,
AddRectShapeArg,
IPAdapterData,
RegionalGuidanceData,
RgbColor,
} from './types';
import { isLine } from './types';
type RegionalGuidanceState = {
_version: 1;
regions: RegionalGuidanceData[];
opacity: number;
};
const initialState: RegionalGuidanceState = {
_version: 1,
regions: [],
opacity: 0.3,
};
const selectRg = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id);
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: RegionalGuidanceState): RgbColor => {
const lastFill = state.regions.slice(-1)[0]?.fill;
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 regionalGuidanceSlice = createSlice({
name: 'regionalGuidance',
initialState,
reducers: {
rgAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg: RegionalGuidanceData = {
id,
type: 'regional_guidance',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
objects: [],
fill: getRGMaskFill(state),
x: 0,
y: 0,
autoNegative: 'invert',
positivePrompt: '',
negativePrompt: null,
ipAdapters: [],
imageCache: null,
};
state.regions.push(rg);
},
prepare: () => ({ payload: { id: uuidv4() } }),
},
rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => {
const { data } = action.payload;
state.regions.push(data);
},
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => {
const { id, isEnabled } = action.payload;
const rg = selectRg(state, id);
if (rg) {
rg.isEnabled = isEnabled;
}
},
rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const rg = selectRg(state, id);
if (rg) {
rg.x = x;
rg.y = y;
}
},
rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const rg = selectRg(state, id);
if (rg) {
rg.bbox = bbox;
rg.bboxNeedsUpdate = false;
}
},
rgDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.regions = state.regions.filter((ca) => ca.id !== id);
},
rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => {
const { opacity } = action.payload;
state.opacity = opacity;
},
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
moveOneToEnd(state.regions, rg);
},
rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
moveToEnd(state.regions, rg);
},
rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
moveOneToStart(state.regions, rg);
},
rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
moveToStart(state.regions, rg);
},
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.positivePrompt = prompt;
},
rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.negativePrompt = prompt;
},
rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => {
const { id, fill } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.fill = fill;
},
rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => {
const { id, imageDTO } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.imageCache = imageDTOToImageWithDims(imageDTO);
},
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
const { id, autoNegative } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.autoNegative = autoNegative;
},
rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => {
const { id, ipAdapter } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.ipAdapters.push(ipAdapter);
},
rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
const { id, ipAdapterId } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
},
rgIPAdapterImageChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
) => {
const { id, ipAdapterId, imageDTO } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => {
const { id, ipAdapterId, weight } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.weight = weight;
},
rgIPAdapterBeginEndStepPctChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }>
) => {
const { id, ipAdapterId, beginEndStepPct } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.beginEndStepPct = beginEndStepPct;
},
rgIPAdapterMethodChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>
) => {
const { id, ipAdapterId, method } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.method = method;
},
rgIPAdapterModelChanged: (
state,
action: PayloadAction<{
id: string;
ipAdapterId: string;
modelConfig: IPAdapterModelConfig | null;
}>
) => {
const { id, ipAdapterId, modelConfig } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
if (modelConfig) {
ipa.model = zModelIdentifierField.parse(modelConfig);
} else {
ipa.model = null;
}
},
rgIPAdapterCLIPVisionModelChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
) => {
const { id, ipAdapterId, clipVisionModel } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.clipVisionModel = clipVisionModel;
},
rgBrushLineAdded: {
reducer: (state, action: PayloadAction<AddBrushLineArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.objects.push({
id: getBrushLineId(id, lineId),
type: 'brush_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y],
strokeWidth: width,
color,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: AddBrushLineArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgEraserLineAdded: {
reducer: (state, action: PayloadAction<AddEraserLineArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.objects.push({
id: getEraserLineId(id, lineId),
type: 'eraser_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y],
strokeWidth: width,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: AddEraserLineArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgLinePointAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
const { id, point } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
return;
}
const lastObject = rg.objects[rg.objects.length - 1];
if (!lastObject || !isLine(lastObject)) {
return;
}
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener
lastObject.points.push(point[0] - rg.x, point[1] - rg.y);
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
rgRectAdded: {
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
return;
}
const rg = selectRg(state, id);
if (!rg) {
return;
}
rg.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
x: rect.x - rg.x,
y: rect.y - rg.y,
width: rect.width,
height: rect.height,
color,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
},
},
});
export const {
rgAdded,
rgRecalled,
rgIsEnabledToggled,
rgTranslated,
rgBboxChanged,
rgDeleted,
rgGlobalOpacityChanged,
rgMovedForwardOne,
rgMovedToFront,
rgMovedBackwardOne,
rgMovedToBack,
rgPositivePromptChanged,
rgNegativePromptChanged,
rgFillChanged,
rgMaskImageUploaded,
rgAutoNegativeChanged,
rgIPAdapterAdded,
rgIPAdapterDeleted,
rgIPAdapterImageChanged,
rgIPAdapterWeightChanged,
rgIPAdapterBeginEndStepPctChanged,
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterCLIPVisionModelChanged,
rgBrushLineAdded,
rgEraserLineAdded,
rgLinePointAdded,
rgRectAdded,
} = regionalGuidanceSlice.actions;
export const selectRegionalGuidanceSlice = (state: RootState) => state.regionalGuidance;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const regionalGuidancePersistConfig: PersistConfig<RegionalGuidanceState> = {
name: regionalGuidanceSlice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -1,9 +1,13 @@
import {
zControlNetConfigV2,
zBeginEndStepPct,
zCLIPVisionModelV2,
zControlModeV2,
zId,
zImageWithDims,
zIPAdapterConfigV2,
zT2IAdapterConfigV2,
zIPMethodV2,
zProcessorConfig,
} from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type {
ParameterHeight,
@ -17,7 +21,6 @@ import {
zAutoNegative,
zParameterNegativePrompt,
zParameterPositivePrompt,
zParameterStrength,
} from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types';
import type { ImageDTO } from 'services/api/types';
@ -31,7 +34,7 @@ const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0,
message: 'Must have an even number of points',
});
const zOLD_VectorMaskLine = z.object({
id: z.string(),
id: zId,
type: z.literal('vector_mask_line'),
tool: zDrawingTool,
strokeWidth: z.number().min(1),
@ -39,7 +42,7 @@ const zOLD_VectorMaskLine = z.object({
});
const zOLD_VectorMaskRect = z.object({
id: z.string(),
id: zId,
type: z.literal('vector_mask_rect'),
x: z.number(),
y: z.number(),
@ -52,6 +55,7 @@ const zRgbColor = z.object({
g: z.number().int().min(0).max(255),
b: z.number().int().min(0).max(255),
});
export type RgbColor = z.infer<typeof zRgbColor>;
const zRgbaColor = zRgbColor.extend({
a: z.number().min(0).max(1),
});
@ -61,7 +65,7 @@ export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };
const zOpacity = z.number().gte(0).lte(1);
const zBrushLine = z.object({
id: z.string(),
id: zId,
type: z.literal('brush_line'),
strokeWidth: z.number().min(1),
points: zPoints,
@ -70,7 +74,7 @@ const zBrushLine = z.object({
export type BrushLine = z.infer<typeof zBrushLine>;
const zEraserline = z.object({
id: z.string(),
id: zId,
type: z.literal('eraser_line'),
strokeWidth: z.number().min(1),
points: zPoints,
@ -78,7 +82,7 @@ const zEraserline = z.object({
export type EraserLine = z.infer<typeof zEraserline>;
const zRectShape = z.object({
id: z.string(),
id: zId,
type: z.literal('rect_shape'),
x: z.number(),
y: z.number(),
@ -89,7 +93,7 @@ const zRectShape = z.object({
export type RectShape = z.infer<typeof zRectShape>;
const zEllipseShape = z.object({
id: z.string(),
id: zId,
type: z.literal('ellipse_shape'),
x: z.number(),
y: z.number(),
@ -100,7 +104,7 @@ const zEllipseShape = z.object({
export type EllipseShape = z.infer<typeof zEllipseShape>;
const zPolygonShape = z.object({
id: z.string(),
id: zId,
type: z.literal('polygon_shape'),
points: zPoints,
color: zRgbaColor,
@ -108,7 +112,7 @@ const zPolygonShape = z.object({
export type PolygonShape = z.infer<typeof zPolygonShape>;
const zImageObject = z.object({
id: z.string(),
id: zId,
type: z.literal('image'),
image: zImageWithDims,
x: z.number(),
@ -118,7 +122,7 @@ const zImageObject = z.object({
});
export type ImageObject = z.infer<typeof zImageObject>;
const zAnyLayerObject = z.discriminatedUnion('type', [
const zLayerObject = z.discriminatedUnion('type', [
zImageObject,
zBrushLine,
zEraserline,
@ -126,13 +130,7 @@ const zAnyLayerObject = z.discriminatedUnion('type', [
zEllipseShape,
zPolygonShape,
]);
export type AnyLayerObject = z.infer<typeof zAnyLayerObject>;
const zLayerBase = z.object({
id: z.string(),
isEnabled: z.boolean().default(true),
isSelected: z.boolean().default(true),
});
export type LayerObject = z.infer<typeof zLayerObject>;
const zRect = z.object({
x: z.number(),
@ -140,33 +138,36 @@ const zRect = z.object({
width: z.number().min(1),
height: z.number().min(1),
});
const zRenderableLayerBase = zLayerBase.extend({
const zLayerData = z.object({
id: zId,
type: z.literal('layer'),
isEnabled: z.boolean(),
x: z.number(),
y: z.number(),
bbox: zRect.nullable(),
bboxNeedsUpdate: z.boolean(),
});
const zRasterLayer = zRenderableLayerBase.extend({
type: z.literal('raster_layer'),
opacity: zOpacity,
objects: z.array(zAnyLayerObject),
objects: z.array(zLayerObject),
});
export type RasterLayer = z.infer<typeof zRasterLayer>;
export type LayerData = z.infer<typeof zLayerData>;
const zControlAdapterLayer = zRenderableLayerBase.extend({
type: z.literal('control_adapter_layer'),
opacity: zOpacity,
isFilterEnabled: z.boolean(),
controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]),
const zIPAdapterData = z.object({
id: zId,
type: z.literal('ip_adapter'),
isEnabled: z.boolean(),
weight: z.number().gte(-1).lte(2),
method: zIPMethodV2,
image: zImageWithDims.nullable(),
model: zModelIdentifierField.nullable(),
clipVisionModel: zCLIPVisionModelV2,
beginEndStepPct: zBeginEndStepPct,
});
export type ControlAdapterLayer = z.infer<typeof zControlAdapterLayer>;
const zIPAdapterLayer = zLayerBase.extend({
type: z.literal('ip_adapter_layer'),
ipAdapter: zIPAdapterConfigV2,
});
export type IPAdapterLayer = z.infer<typeof zIPAdapterLayer>;
export type IPAdapterData = z.infer<typeof zIPAdapterData>;
export type IPAdapterConfig = Pick<
IPAdapterData,
'weight' | 'image' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method'
>;
const zMaskObject = z
.discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zBrushLine, zEraserline, zRectShape])
@ -201,69 +202,109 @@ const zMaskObject = z
})
.pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape]));
const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({
type: z.literal('regional_guidance_layer'),
maskObjects: z.array(zMaskObject),
positivePrompt: zParameterPositivePrompt.nullable(),
negativePrompt: zParameterNegativePrompt.nullable(),
ipAdapters: z.array(zIPAdapterConfigV2),
previewColor: zRgbColor,
autoNegative: zAutoNegative,
uploadedMaskImage: zImageWithDims.nullable(),
});
const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
type: z.literal('regional_guidance_layer'),
const zRegionalGuidanceData = z.object({
id: zId,
type: z.literal('regional_guidance'),
isEnabled: z.boolean(),
x: z.number(),
y: z.number(),
bbox: zRect.nullable(),
bboxNeedsUpdate: z.boolean(),
objects: z.array(zMaskObject),
positivePrompt: zParameterPositivePrompt.nullable(),
negativePrompt: zParameterNegativePrompt.nullable(),
ipAdapters: z.array(zIPAdapterConfigV2),
previewColor: zRgbColor,
ipAdapters: z.array(zIPAdapterData),
fill: zRgbColor,
autoNegative: zAutoNegative,
uploadedMaskImage: zImageWithDims.nullable(),
imageCache: zImageWithDims.nullable(),
});
// TODO(psyche): This doesn't migrate correctly!
const zRGLayer = z
.union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer])
.transform((val) => {
if ('maskObjects' in val) {
const { maskObjects, ...rest } = val;
return { ...rest, objects: maskObjects };
} else {
return val;
}
})
.pipe(zRegionalGuidanceLayer);
export type RegionalGuidanceLayer = z.infer<typeof zRGLayer>;
export type RegionalGuidanceData = z.infer<typeof zRegionalGuidanceData>;
const zInitialImageLayer = zRenderableLayerBase.extend({
type: z.literal('initial_image_layer'),
const zColorFill = z.object({
type: z.literal('color_fill'),
color: zRgbaColor,
});
const zImageFill = z.object({
type: z.literal('image_fill'),
src: z.string(),
});
const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]);
const zInpaintMaskData = z.object({
id: zId,
type: z.literal('inpaint_mask'),
isEnabled: z.boolean(),
x: z.number(),
y: z.number(),
bbox: zRect.nullable(),
bboxNeedsUpdate: z.boolean(),
maskObjects: z.array(zMaskObject),
fill: zFill,
imageCache: zImageWithDims.nullable(),
});
export type InpaintMaskData = z.infer<typeof zInpaintMaskData>;
const zFilter = z.enum(['none', 'lightness_to_alpha']);
export type Filter = z.infer<typeof zFilter>;
const zControlAdapterData = z.object({
id: zId,
type: z.literal('control_adapter'),
isEnabled: z.boolean(),
x: z.number(),
y: z.number(),
bbox: zRect.nullable(),
bboxNeedsUpdate: z.boolean(),
opacity: zOpacity,
filter: zFilter,
weight: z.number().gte(-1).lte(2),
image: zImageWithDims.nullable(),
denoisingStrength: zParameterStrength,
processedImage: zImageWithDims.nullable(),
processorConfig: zProcessorConfig.nullable(),
processorPendingBatchId: z.string().nullable().default(null),
beginEndStepPct: zBeginEndStepPct,
model: zModelIdentifierField.nullable(),
controlMode: zControlModeV2.nullable(),
});
export type InitialImageLayer = z.infer<typeof zInitialImageLayer>;
export type ControlAdapterData = z.infer<typeof zControlAdapterData>;
export type ControlAdapterConfig = Pick<
ControlAdapterData,
'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode'
>;
export const zLayer = z.discriminatedUnion('type', [
zRegionalGuidanceLayer,
zControlAdapterLayer,
zIPAdapterLayer,
zInitialImageLayer,
zRasterLayer,
]);
export type Layer = z.infer<typeof zLayer>;
const zCanvasItemIdentifier = z.object({
type: z.enum([
zLayerData.shape.type.value,
zIPAdapterData.shape.type.value,
zControlAdapterData.shape.type.value,
zRegionalGuidanceData.shape.type.value,
zInpaintMaskData.shape.type.value,
]),
id: zId,
});
type CanvasItemIdentifier = z.infer<typeof zCanvasItemIdentifier>;
export type ControlLayersState = {
export type CanvasV2State = {
_version: 3;
selectedLayerId: string | null;
layers: Layer[];
brushSize: number;
brushColor: RgbaColor;
globalMaskLayerOpacity: number;
positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt;
positivePrompt2: ParameterPositiveStylePromptSDXL;
negativePrompt2: ParameterNegativeStylePromptSDXL;
shouldConcatPrompts: boolean;
lastSelectedItem: CanvasItemIdentifier | null;
prompts: {
positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt;
positivePrompt2: ParameterPositiveStylePromptSDXL;
negativePrompt2: ParameterNegativeStylePromptSDXL;
shouldConcatPrompts: boolean;
};
tool: {
selected: Tool;
selectedBuffer: Tool | null;
invertScroll: boolean;
brush: {
width: number;
};
eraser: {
width: number;
};
fill: RgbaColor;
};
size: {
width: ParameterWidth;
height: ParameterHeight;
@ -273,45 +314,13 @@ export type ControlLayersState = {
};
export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number };
export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] };
export type AddEraserLineArg = { id: string; points: [number, number, number, number]; width: number };
export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor };
export type AddPointToLineArg = { layerId: string; point: [number, number] };
export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor };
export type AddImageObjectArg = { layerId: string; imageDTO: ImageDTO };
export type AddPointToLineArg = { id: string; point: [number, number] };
export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor };
export type AddImageObjectArg = { id: string; imageDTO: ImageDTO };
//#region Type guards
export const isLine = (obj: AnyLayerObject): obj is BrushLine | EraserLine => {
export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => {
return obj.type === 'brush_line' || obj.type === 'eraser_line';
};
export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => {
return layer?.type === 'regional_guidance_layer';
};
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => {
return layer?.type === 'control_adapter_layer';
};
export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => {
return layer?.type === 'ip_adapter_layer';
};
export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => {
return layer?.type === 'initial_image_layer';
};
export const isRasterLayer = (layer?: Layer): layer is RasterLayer => {
return layer?.type === 'raster_layer';
};
export const isRenderableLayer = (
layer?: Layer
): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer | RasterLayer => {
return (
isRegionalGuidanceLayer(layer) || isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer)
);
};
export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => {
return isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer);
};
export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => {
return isControlAdapterLayer(layer) || isIPAdapterLayer(layer);
};
export const isRGOrRasterlayer = (layer?: Layer): layer is RegionalGuidanceLayer | RasterLayer => {
return isRegionalGuidanceLayer(layer) || isRasterLayer(layer);
};
//#endregion

View File

@ -10,7 +10,7 @@ import type {
} from 'services/api/types';
import { z } from 'zod';
const zId = z.string().min(1);
export const zId = z.string().min(1);
const zCannyProcessorConfig = z.object({
id: zId,
@ -120,7 +120,7 @@ const zZoeDepthProcessorConfig = z.object({
});
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
const zProcessorConfig = z.discriminatedUnion('type', [
export const zProcessorConfig = z.discriminatedUnion('type', [
zCannyProcessorConfig,
zColorMapProcessorConfig,
zContentShuffleProcessorConfig,
@ -145,7 +145,7 @@ export const zImageWithDims = z.object({
});
export type ImageWithDims = z.infer<typeof zImageWithDims>;
const zBeginEndStepPct = z
export const zBeginEndStepPct = z
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
.refine(([begin, end]) => begin < end, {
message: 'Begin must be less than end',
@ -161,7 +161,7 @@ const zControlAdapterBase = z.object({
beginEndStepPct: zBeginEndStepPct,
});
const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
export const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
@ -178,11 +178,11 @@ export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({
});
export type T2IAdapterConfigV2 = z.infer<typeof zT2IAdapterConfigV2>;
const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success;
const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
export const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;

View File

@ -3,7 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import {
@ -27,7 +27,7 @@ const selectImageUsages = createMemoizedSelector(
selectCanvasSlice,
selectNodesSlice,
selectControlAdaptersSlice,
selectControlLayersSlice,
selectCanvasV2Slice,
selectImageUsage,
],
(deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => {

View File

@ -7,8 +7,8 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdaptersState } from 'features/controlAdapters/store/types';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import type { ControlLayersState } from 'features/controlLayers/store/types';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import type { CanvasV2State } from 'features/controlLayers/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
@ -28,7 +28,7 @@ export const getImageUsage = (
canvas: CanvasState,
nodes: NodesState,
controlAdapters: ControlAdaptersState,
controlLayers: ControlLayersState,
controlLayers: CanvasV2State,
image_name: string
) => {
const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name);
@ -75,7 +75,7 @@ export const selectImageUsage = createMemoizedSelector(
selectCanvasSlice,
selectNodesSlice,
selectControlAdaptersSlice,
selectControlLayersSlice,
selectCanvasV2Slice,
(deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => {
const { imagesToDelete } = deleteImageModal;

View File

@ -15,7 +15,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
@ -42,7 +42,7 @@ const DeleteBoardModal = (props: Props) => {
const selectImageUsageSummary = useMemo(
() =>
createMemoizedSelector(
[selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice],
[selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice],
(canvas, nodes, controlAdapters, controlLayers) => {
const allImageUsage = (boardImageNames ?? []).map((imageName) =>
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName)

View File

@ -1,4 +1,4 @@
import type { Layer } from 'features/controlLayers/store/types';
import type { LayerData } from 'features/controlLayers/store/types';
import { MetadataItemView } from 'features/metadata/components/MetadataItemView';
import type { MetadataHandlers } from 'features/metadata/types';
import { handlers } from 'features/metadata/util/handlers';
@ -9,7 +9,7 @@ type Props = {
};
export const MetadataLayers = ({ metadata }: Props) => {
const [layers, setLayers] = useState<Layer[]>([]);
const [layers, setLayers] = useState<LayerData[]>([]);
useEffect(() => {
const parse = async () => {
@ -40,8 +40,8 @@ const MetadataViewLayer = ({
handlers,
}: {
label: string;
layer: Layer;
handlers: MetadataHandlers<Layer[], Layer>;
layer: LayerData;
handlers: MetadataHandlers<LayerData[], LayerData>;
}) => {
const onRecall = useCallback(() => {
if (!handlers.recallItem) {

View File

@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
import { objectKeys } from 'common/util/objectKeys';
import { shouldConcatPromptsChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import type { LayerData } from 'features/controlLayers/store/types';
import type { LoRA } from 'features/lora/store/loraSlice';
import type {
AnyControlAdapterConfigMetadata,
@ -49,7 +49,7 @@ const renderControlAdapterValue: MetadataRenderValueFunc<AnyControlAdapterConfig
return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`;
}
};
const renderLayerValue: MetadataRenderValueFunc<Layer> = async (layer) => {
const renderLayerValue: MetadataRenderValueFunc<LayerData> = async (layer) => {
if (layer.type === 'initial_image_layer') {
let rendered = t('controlLayers.globalInitialImageLayer');
if (layer.image) {
@ -89,7 +89,7 @@ const renderLayerValue: MetadataRenderValueFunc<Layer> = async (layer) => {
}
assert(false, 'Unknown layer type');
};
const renderLayersValue: MetadataRenderValueFunc<Layer[]> = async (layers) => {
const renderLayersValue: MetadataRenderValueFunc<LayerData[]> = async (layers) => {
return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`;
};

View File

@ -5,7 +5,7 @@ import {
} from 'features/controlAdapters/util/buildControlAdapter';
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/konva/naming';
import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, Layer } from 'features/controlLayers/store/types';
import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, LayerData } from 'features/controlLayers/store/types';
import { zLayer } from 'features/controlLayers/store/types';
import {
CA_PROCESSOR_DATA,
@ -431,22 +431,22 @@ const parseAllIPAdapters: MetadataParseFunc<IPAdapterConfigMetadata[]> = async (
};
//#region Control Layers
const parseLayer: MetadataParseFunc<Layer> = async (metadataItem) => zLayer.parseAsync(metadataItem);
const parseLayer: MetadataParseFunc<LayerData> = async (metadataItem) => zLayer.parseAsync(metadataItem);
const parseLayers: MetadataParseFunc<Layer[]> = async (metadata) => {
const parseLayers: MetadataParseFunc<LayerData[]> = async (metadata) => {
// We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles
// taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For
// example, CL Control Adapters don't support resize mode, so we simply omit that property.
try {
const layers: Layer[] = [];
const layers: LayerData[] = [];
try {
const control_layers = await getProperty(metadata, 'control_layers');
const controlLayersRaw = await getProperty(control_layers, 'layers', isArray);
const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer));
const controlLayers = controlLayersParseResults
.filter((result): result is PromiseFulfilledResult<Layer> => result.status === 'fulfilled')
.filter((result): result is PromiseFulfilledResult<LayerData> => result.status === 'fulfilled')
.map((result) => result.value);
layers.push(...controlLayers);
} catch {

View File

@ -9,18 +9,18 @@ import {
import { getCALayerId, getIPALayerId, getRGLayerId } from 'features/controlLayers/konva/naming';
import {
allLayersDeleted,
caLayerRecalled,
controlAdapterRecalled,
heightChanged,
iiLayerRecalled,
ipaLayerRecalled,
ipAdapterRecalled,
negativePrompt2Changed,
negativePromptChanged,
positivePrompt2Changed,
positivePromptChanged,
rgLayerRecalled,
regionalGuidanceRecalled,
widthChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import type { LayerData } from 'features/controlLayers/store/types';
import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice';
import type { LoRA } from 'features/lora/store/loraSlice';
import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice';
@ -242,7 +242,7 @@ const recallIPAdapters: MetadataRecallFunc<IPAdapterConfigMetadata[]> = (ipAdapt
};
//#region Control Layers
const recallLayer: MetadataRecallFunc<Layer> = async (layer) => {
const recallLayer: MetadataRecallFunc<LayerData> = async (layer) => {
const { dispatch } = getStore();
// We need to check for the existence of all images and models when recalling. If they do not exist, SMITE THEM!
// Also, we need fresh IDs for all objects when recalling, to prevent multiple layers with the same ID.
@ -269,7 +269,7 @@ const recallLayer: MetadataRecallFunc<Layer> = async (layer) => {
}
clone.id = getCALayerId(uuidv4());
clone.controlAdapter.id = uuidv4();
dispatch(caLayerRecalled(clone));
dispatch(controlAdapterRecalled(clone));
return;
}
if (layer.type === 'ip_adapter_layer') {
@ -289,7 +289,7 @@ const recallLayer: MetadataRecallFunc<Layer> = async (layer) => {
}
clone.id = getIPALayerId(uuidv4());
clone.ipAdapter.id = uuidv4();
dispatch(ipaLayerRecalled(clone));
dispatch(ipAdapterRecalled(clone));
return;
}
@ -315,7 +315,7 @@ const recallLayer: MetadataRecallFunc<Layer> = async (layer) => {
ipAdapter.id = uuidv4();
}
clone.id = getRGLayerId(uuidv4());
dispatch(rgLayerRecalled(clone));
dispatch(regionalGuidanceRecalled(clone));
return;
}
@ -325,7 +325,7 @@ const recallLayer: MetadataRecallFunc<Layer> = async (layer) => {
}
};
const recallLayers: MetadataRecallFunc<Layer[]> = (layers) => {
const recallLayers: MetadataRecallFunc<LayerData[]> = (layers) => {
const { dispatch } = getStore();
dispatch(allLayersDeleted());
for (const l of layers) {

View File

@ -1,5 +1,5 @@
import { getStore } from 'app/store/nanostores/store';
import type { Layer } from 'features/controlLayers/store/types';
import type { LayerData } from 'features/controlLayers/store/types';
import type { LoRA } from 'features/lora/store/loraSlice';
import type {
ControlNetConfigMetadata,
@ -110,7 +110,7 @@ const validateIPAdapters: MetadataValidateFunc<IPAdapterConfigMetadata[]> = (ipA
return new Promise((resolve) => resolve(validatedIPAdapters));
};
const validateLayer: MetadataValidateFunc<Layer> = async (layer) => {
const validateLayer: MetadataValidateFunc<LayerData> = async (layer) => {
if (layer.type === 'control_adapter_layer') {
const model = layer.controlAdapter.model;
assert(model, 'Control Adapter layer missing model');
@ -132,8 +132,8 @@ const validateLayer: MetadataValidateFunc<Layer> = async (layer) => {
return layer;
};
const validateLayers: MetadataValidateFunc<Layer[]> = async (layers) => {
const validatedLayers: Layer[] = [];
const validateLayers: MetadataValidateFunc<LayerData[]> = async (layers) => {
const validatedLayers: LayerData[] = [];
for (const l of layers) {
try {
const validated = await validateLayer(l);

View File

@ -5,8 +5,8 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming';
import { renderers } from 'features/controlLayers/konva/renderers/layers';
import { rgLayerMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice';
import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice';
import type { InitialImageLayer, LayerData, RegionalGuidanceLayer } from 'features/controlLayers/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
@ -70,7 +70,7 @@ export const addControlLayers = async (
| Invocation<'vae_loader'>
| Invocation<'main_model_loader'>
| Invocation<'sdxl_model_loader'>
): Promise<Layer[]> => {
): Promise<LayerData[]> => {
const isSDXL = base === 'sdxl';
const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base));
@ -492,7 +492,7 @@ const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean
return hasModel && modelMatchesBase && hasImage;
};
const isValidLayer = (layer: Layer, base: BaseModelType) => {
const isValidLayer = (layer: LayerData, base: BaseModelType) => {
if (!layer.isEnabled) {
return false;
}
@ -532,7 +532,7 @@ const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise<I
req.reset();
const imageDTO = await req.unwrap();
dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO }));
dispatch(regionalGuidanceMaskImageUploaded({ layerId: layer.id, imageDTO }));
return imageDTO;
};
//#endregion

View File

@ -2,7 +2,7 @@ import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-a
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import type { PropsWithChildren } from 'react';
@ -12,7 +12,7 @@ import { useEnqueueBatchMutation } from 'services/api/endpoints/queue';
import { useBoardName } from 'services/api/hooks/useBoardName';
const selectPromptsCount = createSelector(
selectControlLayersSlice,
selectCanvasV2Slice,
selectDynamicPromptsSlice,
(controlLayers, dynamicPrompts) =>
getShouldProcessPrompt(controlLayers.present.positivePrompt) ? dynamicPrompts.prompts.length : 1

View File

@ -3,7 +3,7 @@ import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-a
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { HrfSettings } from 'features/hrf/components/HrfSettings';
import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
@ -24,7 +24,7 @@ import { ImageSizeCanvas } from './ImageSizeCanvas';
import { ImageSizeLinear } from './ImageSizeLinear';
const selector = createMemoizedSelector(
[selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectControlLayersSlice, activeTabNameSelector],
[selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector],
(generation, canvas, hrf, controlLayers, activeTabName) => {
const { shouldRandomizeSeed, model } = generation;
const { hrfEnabled } = hrf;