refactor(ui): canvas v2 (wip)

This commit is contained in:
psychedelicious 2024-06-14 14:19:35 +10:00
parent 6c1d1588fc
commit c51253f5f6
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 type { AppDispatch } from 'app/store/store';
import { parseify } from 'common/util/serialize'; import { parseify } from 'common/util/serialize';
import { import {
caLayerImageChanged, controlAdapterImageChanged,
caLayerModelChanged, controlAdapterModelChanged,
caLayerProcessedImageChanged, controlAdapterProcessedImageChanged,
caLayerProcessorConfigChanged, controlAdapterProcessorConfigChanged,
caLayerProcessorPendingBatchIdChanged, controlAdapterProcessorPendingBatchIdChanged,
caLayerRecalled, controlAdapterRecalled,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import { isControlAdapterLayer } from 'features/controlLayers/store/types';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
@ -23,11 +23,11 @@ import { socketInvocationComplete } from 'services/events/actions';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
const matcher = isAnyOf( const matcher = isAnyOf(
caLayerImageChanged, controlAdapterImageChanged,
caLayerProcessedImageChanged, controlAdapterProcessedImageChanged,
caLayerProcessorConfigChanged, controlAdapterProcessorConfigChanged,
caLayerModelChanged, controlAdapterModelChanged,
caLayerRecalled controlAdapterRecalled
); );
const DEBOUNCE_MS = 300; const DEBOUNCE_MS = 300;
@ -46,7 +46,7 @@ const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batc
} finally { } finally {
req.reset(); req.reset();
// Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist // 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({ startAppListening({
matcher, matcher,
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => { 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 state = getState();
const originalState = getOriginalState(); 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 image, we have nothing to process
// - If we have no processor config, we have nothing to process // - If we have no processor config, we have nothing to process
// Clear the processed image and bail // Clear the processed image and bail
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null }));
return; return;
} }
@ -132,7 +132,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
const enqueueResult = await req.unwrap(); 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 // 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'); 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')); log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
// Wait for the processor node to complete // 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 // Whew! We made it. Update the layer with the processed image
log.debug({ layerId, imageDTO }, 'ControlNet image processed'); log.debug({ layerId, imageDTO }, 'ControlNet image processed');
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO })); dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO }));
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null }));
} catch (error) { } catch (error) {
if (signal.aborted) { if (signal.aborted) {
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). // 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 (error instanceof Object) {
if ('data' in error && 'status' in error) { if ('data' in error && 'status' in error) {
if (error.status === 403) { if (error.status === 403) {
dispatch(caLayerImageChanged({ layerId, imageDTO: null })); dispatch(controlAdapterImageChanged({ layerId, imageDTO: null }));
return; return;
} }
} }

View File

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

View File

@ -6,10 +6,10 @@ import {
controlAdapterIsEnabledChanged, controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { import {
caLayerImageChanged, controlAdapterImageChanged,
iiLayerImageChanged, iiLayerImageChanged,
ipaLayerImageChanged, ipAdapterImageChanged,
rgLayerIPAdapterImageChanged, regionalGuidanceIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
@ -122,7 +122,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
const { layerId } = postUploadAction; const { layerId } = postUploadAction;
dispatch(caLayerImageChanged({ layerId, imageDTO })); dispatch(controlAdapterImageChanged({ layerId, imageDTO }));
toast({ toast({
...DEFAULT_UPLOADED_TOAST, ...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'), description: t('toast.setControlImage'),
@ -131,7 +131,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
const { layerId } = postUploadAction; const { layerId } = postUploadAction;
dispatch(ipaLayerImageChanged({ layerId, imageDTO })); dispatch(ipAdapterImageChanged({ layerId, imageDTO }));
toast({ toast({
...DEFAULT_UPLOADED_TOAST, ...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'), description: t('toast.setControlImage'),
@ -140,7 +140,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
const { layerId, ipAdapterId } = postUploadAction; const { layerId, ipAdapterId } = postUploadAction;
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
toast({ toast({
...DEFAULT_UPLOADED_TOAST, ...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'), 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 { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { import {
controlAdaptersPersistConfig, controlAdaptersV2PersistConfig,
controlAdaptersSlice, controlAdaptersV2Slice,
} from 'features/controlAdapters/store/controlAdaptersSlice'; } 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 { import {
controlLayersPersistConfig, regionalGuidancePersistConfig,
controlLayersSlice, regionalGuidanceSlice,
controlLayersUndoableConfig, } from 'features/controlLayers/store/regionalGuidanceSlice';
} from 'features/controlLayers/store/controlLayersSlice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
@ -49,6 +51,7 @@ import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware'; import { listenerMiddleware } from './middleware/listenerMiddleware';
const allReducers = { const allReducers = {
[api.reducerPath]: api.reducer,
[canvasSlice.name]: canvasSlice.reducer, [canvasSlice.name]: canvasSlice.reducer,
[gallerySlice.name]: gallerySlice.reducer, [gallerySlice.name]: gallerySlice.reducer,
[generationSlice.name]: generationSlice.reducer, [generationSlice.name]: generationSlice.reducer,
@ -56,7 +59,6 @@ const allReducers = {
[systemSlice.name]: systemSlice.reducer, [systemSlice.name]: systemSlice.reducer,
[configSlice.name]: configSlice.reducer, [configSlice.name]: configSlice.reducer,
[uiSlice.name]: uiSlice.reducer, [uiSlice.name]: uiSlice.reducer,
[controlAdaptersSlice.name]: controlAdaptersSlice.reducer,
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer, [dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
[deleteImageModalSlice.name]: deleteImageModalSlice.reducer, [deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer, [changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
@ -66,11 +68,14 @@ const allReducers = {
[queueSlice.name]: queueSlice.reducer, [queueSlice.name]: queueSlice.reducer,
[workflowSlice.name]: workflowSlice.reducer, [workflowSlice.name]: workflowSlice.reducer,
[hrfSlice.name]: hrfSlice.reducer, [hrfSlice.name]: hrfSlice.reducer,
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), [canvasV2Slice.name]: canvasV2Slice.reducer,
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[api.reducerPath]: api.reducer,
[upscaleSlice.name]: upscaleSlice.reducer, [upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.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); const rootReducer = combineReducers(allReducers);
@ -107,16 +112,19 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[systemPersistConfig.name]: systemPersistConfig, [systemPersistConfig.name]: systemPersistConfig,
[workflowPersistConfig.name]: workflowPersistConfig, [workflowPersistConfig.name]: workflowPersistConfig,
[uiPersistConfig.name]: uiPersistConfig, [uiPersistConfig.name]: uiPersistConfig,
[controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig,
[dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig,
[sdxlPersistConfig.name]: sdxlPersistConfig, [sdxlPersistConfig.name]: sdxlPersistConfig,
[loraPersistConfig.name]: loraPersistConfig, [loraPersistConfig.name]: loraPersistConfig,
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
[hrfPersistConfig.name]: hrfPersistConfig, [hrfPersistConfig.name]: hrfPersistConfig,
[controlLayersPersistConfig.name]: controlLayersPersistConfig, [canvasV2PersistConfig.name]: canvasV2PersistConfig,
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
[upscalePersistConfig.name]: upscalePersistConfig, [upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig,
[layersPersistConfig.name]: layersPersistConfig,
[controlAdaptersV2PersistConfig.name]: controlAdaptersV2PersistConfig,
[ipAdaptersPersistConfig.name]: ipAdaptersPersistConfig,
[regionalGuidancePersistConfig.name]: regionalGuidancePersistConfig,
}; };
const unserialize: UnserializeFunction = (data, key) => { const unserialize: UnserializeFunction = (data, key) => {

View File

@ -6,8 +6,8 @@ import {
selectControlAdaptersSlice, selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types'; import type { LayerData } from 'features/controlLayers/store/types';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
@ -24,7 +24,7 @@ import { forEach, upperFirst } from 'lodash-es';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow'; 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', initial_image_layer: 'controlLayers.globalInitialImage',
control_adapter_layer: 'controlLayers.globalControlAdapter', control_adapter_layer: 'controlLayers.globalControlAdapter',
ip_adapter_layer: 'controlLayers.globalIPAdapter', ip_adapter_layer: 'controlLayers.globalIPAdapter',
@ -41,7 +41,7 @@ const createSelector = (templates: Templates) =>
selectNodesSlice, selectNodesSlice,
selectWorkflowSettingsSlice, selectWorkflowSettingsSlice,
selectDynamicPromptsSlice, selectDynamicPromptsSlice,
selectControlLayersSlice, selectCanvasV2Slice,
activeTabNameSelector, activeTabNameSelector,
selectUpscalelice, selectUpscalelice,
selectConfigSlice, 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'; import { describe, expect, it } from 'vitest';
describe('Array Manipulation Functions', () => { describe('Array Manipulation Functions', () => {
const originalArray = ['a', 'b', 'c', 'd']; 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', () => { describe('moveOneToEnd', () => {
const array = [...originalArray]; describe('with callback', () => {
const result = moveForward(array, (item) => item === 'd'); it('should move an item forward by one position', () => {
expect(result).toEqual(['a', 'b', 'c', 'd']); 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", () => { it('should do nothing if the item is at the end', () => {
const array = [...originalArray]; const array = [...originalArray];
const result = moveForward(array, (item) => item === 'z'); const result = moveOneToEnd(array, (item) => item === 'd');
expect(result).toEqual(originalArray); 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', () => { describe('moveToStart', () => {
it('should move an item to the front', () => { describe('with callback', () => {
const array = [...originalArray]; it('should move an item to the front', () => {
const result = moveToFront(array, (item) => item === 'c'); const array = [...originalArray];
expect(result).toEqual(['c', 'a', 'b', 'd']); 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', () => { it('should do nothing if the item is already at the front', () => {
const array = [...originalArray]; const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'a'); const result = moveToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']); expect(result).toEqual(['a', 'b', 'c', 'd']);
}); });
it("should leave the array unchanged if the item isn't in the array", () => { it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray]; const array = [...originalArray];
const result = moveToFront(array, (item) => item === 'z'); const result = moveToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray); 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', () => { describe('moveOneToStart', () => {
it('should move an item backward by one position', () => { describe('with callback', () => {
const array = [...originalArray]; it('should move an item backward by one position', () => {
const result = moveBackward(array, (item) => item === 'c'); const array = [...originalArray];
expect(result).toEqual(['a', 'c', 'b', 'd']); 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', () => { it('should do nothing if the item is at the beginning', () => {
const array = [...originalArray]; const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'a'); const result = moveOneToStart(array, (item) => item === 'a');
expect(result).toEqual(['a', 'b', 'c', 'd']); expect(result).toEqual(['a', 'b', 'c', 'd']);
}); });
it("should leave the array unchanged if the item isn't in the array", () => { it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray]; const array = [...originalArray];
const result = moveBackward(array, (item) => item === 'z'); const result = moveOneToStart(array, (item) => item === 'z');
expect(result).toEqual(originalArray); 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', () => { describe('moveToEnd', () => {
it('should move an item to the back', () => { describe('with callback', () => {
const array = [...originalArray]; it('should move an item to the back', () => {
const result = moveToBack(array, (item) => item === 'b'); const array = [...originalArray];
expect(result).toEqual(['a', 'c', 'd', 'b']); 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', () => { it('should do nothing if the item is already at the back', () => {
const array = [...originalArray]; const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'd'); const result = moveToEnd(array, (item) => item === 'd');
expect(result).toEqual(['a', 'b', 'c', 'd']); expect(result).toEqual(['a', 'b', 'c', 'd']);
}); });
it("should leave the array unchanged if the item isn't in the array", () => { it("should leave the array unchanged if the item isn't in the array", () => {
const array = [...originalArray]; const array = [...originalArray];
const result = moveToBack(array, (item) => item === 'z'); const result = moveToEnd(array, (item) => item === 'z');
expect(result).toEqual(originalArray); 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[] => { export function moveToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
const index = array.findIndex(callback); export function moveToStart<T>(array: T[], item: T): T[];
if (index >= 0 && index < array.length - 1) { export function moveToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
//@ts-expect-error - These indicies are safe per the previous check const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
[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);
if (index > 0) { if (index > 0) {
const [item] = array.splice(index, 1); const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check //@ts-expect-error - These indicies are safe per the previous check
array.unshift(item); array.unshift(item);
} }
return array; return array;
}; }
export const moveBackward = <T>(array: T[], callback: (item: T) => boolean): T[] => { export function moveOneToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
const index = array.findIndex(callback); 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) { if (index > 0) {
//@ts-expect-error - These indicies are safe per the previous check //@ts-expect-error - These indicies are safe per the previous check
[array[index], array[index - 1]] = [array[index - 1], array[index]]; [array[index], array[index - 1]] = [array[index - 1], array[index]];
} }
return array; return array;
}; }
export const moveToBack = <T>(array: T[], callback: (item: T) => boolean): T[] => { export function moveToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
const index = array.findIndex(callback); 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) { if (index >= 0 && index < array.length - 1) {
const [item] = array.splice(index, 1); const [item] = array.splice(index, 1);
//@ts-expect-error - These indicies are safe per the previous check //@ts-expect-error - These indicies are safe per the previous check
array.push(item); array.push(item);
} }
return array; 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 { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; 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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi'; import { PiPlusBold } from 'react-icons/pi';
@ -13,10 +13,10 @@ export const AddLayerButton = memo(() => {
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); const [addIILayer, isAddIILayerDisabled] = useAddIILayer();
const addRGLayer = useCallback(() => { const addRGLayer = useCallback(() => {
dispatch(rgLayerAdded()); dispatch(regionalGuidanceAdded());
}, [dispatch]); }, [dispatch]);
const addRasterLayer = useCallback(() => { const addRasterLayer = useCallback(() => {
dispatch(rasterLayerAdded()); dispatch(layerAdded());
}, [dispatch]); }, [dispatch]);
return ( return (

View File

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

View File

@ -1,13 +1,13 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter'; import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter';
import { import {
caLayerControlModeChanged,
caLayerImageChanged,
caLayerModelChanged,
caLayerProcessedImageChanged,
caLayerProcessorConfigChanged,
caOrIPALayerBeginEndStepPctChanged, caOrIPALayerBeginEndStepPctChanged,
caOrIPALayerWeightChanged, caOrIPALayerWeightChanged,
controlAdapterControlModeChanged,
controlAdapterImageChanged,
controlAdapterModelChanged,
controlAdapterProcessedImageChanged,
controlAdapterProcessorConfigChanged,
selectLayerOrThrow, selectLayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import { isControlAdapterLayer } from 'features/controlLayers/store/types';
@ -46,7 +46,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeControlMode = useCallback( const onChangeControlMode = useCallback(
(controlMode: ControlModeV2) => { (controlMode: ControlModeV2) => {
dispatch( dispatch(
caLayerControlModeChanged({ controlAdapterControlModeChanged({
layerId, layerId,
controlMode, controlMode,
}) })
@ -64,7 +64,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeProcessorConfig = useCallback( const onChangeProcessorConfig = useCallback(
(processorConfig: ProcessorConfig | null) => { (processorConfig: ProcessorConfig | null) => {
dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); dispatch(controlAdapterProcessorConfigChanged({ layerId, processorConfig }));
}, },
[dispatch, layerId] [dispatch, layerId]
); );
@ -72,7 +72,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeModel = useCallback( const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
dispatch( dispatch(
caLayerModelChanged({ controlAdapterModelChanged({
layerId, layerId,
modelConfig, modelConfig,
}) })
@ -83,17 +83,17 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const onChangeImage = useCallback( const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => { (imageDTO: ImageDTO | null) => {
dispatch(caLayerImageChanged({ layerId, imageDTO })); dispatch(controlAdapterImageChanged({ layerId, imageDTO }));
}, },
[dispatch, layerId] [dispatch, layerId]
); );
const onErrorLoadingImage = useCallback(() => { const onErrorLoadingImage = useCallback(() => {
dispatch(caLayerImageChanged({ layerId, imageDTO: null })); dispatch(controlAdapterImageChanged({ layerId, imageDTO: null }));
}, [dispatch, layerId]); }, [dispatch, layerId]);
const onErrorLoadingProcessedImage = useCallback(() => { const onErrorLoadingProcessedImage = useCallback(() => {
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null }));
}, [dispatch, layerId]); }, [dispatch, layerId]);
const droppableData = useMemo<CALayerImageDropData>( 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 { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer';
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types'; import type { LayerData } from 'features/controlLayers/store/types';
import { isRenderableLayer } from 'features/controlLayers/store/types'; import { isRenderableLayer } from 'features/controlLayers/store/types';
import { partition } from 'lodash-es'; import { partition } from 'lodash-es';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer);
return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse(); return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse();
}); });
@ -50,7 +50,7 @@ ControlLayersPanelContent.displayName = 'ControlLayersPanelContent';
type LayerWrapperProps = { type LayerWrapperProps = {
id: string; id: string;
type: Layer['type']; type: LayerData['type'];
}; };
const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { Text } from '@invoke-ai/ui-library'; 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 { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type Props = { type Props = {
type: Layer['type']; type: LayerData['type'];
}; };
export const LayerTitle = memo(({ type }: Props) => { 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 { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; 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 { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -29,7 +29,7 @@ export const RGLayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selector = useMemo( const selector = useMemo(
() => () =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return { return {

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper'; 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 { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
@ -14,7 +14,7 @@ type Props = {
export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
const selectIPAdapterIds = useMemo( const selectIPAdapterIds = useMemo(
() => () =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(layer, `Layer ${layerId} not found`);
return layer.ipAdapters; 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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter';
import { import {
rgLayerIPAdapterBeginEndStepPctChanged, regionalGuidanceIPAdapterBeginEndStepPctChanged,
rgLayerIPAdapterCLIPVisionModelChanged, regionalGuidanceIPAdapterCLIPVisionModelChanged,
rgLayerIPAdapterDeleted, regionalGuidanceIPAdapterDeleted,
rgLayerIPAdapterImageChanged, regionalGuidanceIPAdapterImageChanged,
rgLayerIPAdapterMethodChanged, regionalGuidanceIPAdapterMethodChanged,
rgLayerIPAdapterModelChanged, regionalGuidanceIPAdapterModelChanged,
rgLayerIPAdapterWeightChanged, regionalGuidanceIPAdapterWeightChanged,
selectRGLayerIPAdapterOrThrow, selectRGLayerIPAdapterOrThrow,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
@ -26,14 +26,14 @@ type Props = {
export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => { export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => { const onDeleteIPAdapter = useCallback(() => {
dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId }));
}, [dispatch, ipAdapterId, layerId]); }, [dispatch, ipAdapterId, layerId]);
const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId)); const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId));
const onChangeBeginEndStepPct = useCallback( const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => { (beginEndStepPct: [number, number]) => {
dispatch( dispatch(
rgLayerIPAdapterBeginEndStepPctChanged({ regionalGuidanceIPAdapterBeginEndStepPctChanged({
layerId, layerId,
ipAdapterId, ipAdapterId,
beginEndStepPct, beginEndStepPct,
@ -45,35 +45,35 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
const onChangeWeight = useCallback( const onChangeWeight = useCallback(
(weight: number) => { (weight: number) => {
dispatch(rgLayerIPAdapterWeightChanged({ layerId, ipAdapterId, weight })); dispatch(regionalGuidanceIPAdapterWeightChanged({ layerId, ipAdapterId, weight }));
}, },
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]
); );
const onChangeIPMethod = useCallback( const onChangeIPMethod = useCallback(
(method: IPMethodV2) => { (method: IPMethodV2) => {
dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method })); dispatch(regionalGuidanceIPAdapterMethodChanged({ layerId, ipAdapterId, method }));
}, },
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]
); );
const onChangeModel = useCallback( const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => { (modelConfig: IPAdapterModelConfig) => {
dispatch(rgLayerIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig })); dispatch(regionalGuidanceIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig }));
}, },
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]
); );
const onChangeCLIPVisionModel = useCallback( const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => { (clipVisionModel: CLIPVisionModelV2) => {
dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); dispatch(regionalGuidanceIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel }));
}, },
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]
); );
const onChangeImage = useCallback( const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => { (imageDTO: ImageDTO | null) => {
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
}, },
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import type {
AddEraserLineArg, AddEraserLineArg,
AddPointToLineArg, AddPointToLineArg,
AddRectShapeArg, AddRectShapeArg,
Layer, LayerData,
StageAttrs, StageAttrs,
Tool, Tool,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
@ -38,7 +38,7 @@ type Arg = {
getBrushColor: () => RgbaColor; getBrushColor: () => RgbaColor;
getBrushSize: () => number; getBrushSize: () => number;
getBrushSpacingPx: () => number; getBrushSpacingPx: () => number;
getSelectedLayer: () => Layer | null; getSelectedLayer: () => LayerData | null;
getShouldInvert: () => boolean; getShouldInvert: () => boolean;
getSpaceKey: () => boolean; getSpaceKey: () => boolean;
onBrushLineAdded: (arg: AddBrushLineArg) => void; 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 * @param onPointAddedToLine The callback to add a point to a line
*/ */
const maybeAddNextPoint = ( const maybeAddNextPoint = (
layerId: string, selectedLayer: LayerData,
currentPos: Vector2d, currentPos: Vector2d,
getLastAddedPoint: Arg['getLastAddedPoint'], getLastAddedPoint: Arg['getLastAddedPoint'],
setLastAddedPoint: Arg['setLastAddedPoint'], setLastAddedPoint: Arg['setLastAddedPoint'],
@ -88,7 +88,7 @@ const maybeAddNextPoint = (
} }
} }
setLastAddedPoint(currentPos); setLastAddedPoint(currentPos);
onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] }); onPointAddedToLine({ layerId, point: [currentPos.x - selectedLayer.x, currentPos.y - selectedLayer.y] });
}; };
export const setStageEventHandlers = ({ export const setStageEventHandlers = ({
@ -158,7 +158,7 @@ export const setStageEventHandlers = ({
if (tool === 'brush') { if (tool === 'brush') {
onBrushLineAdded({ onBrushLineAdded({
layerId: selectedLayer.id, 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, color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
}); });
} }
@ -166,7 +166,7 @@ export const setStageEventHandlers = ({
if (tool === 'eraser') { if (tool === 'eraser') {
onEraserLineAdded({ onEraserLineAdded({
layerId: selectedLayer.id, 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 // Start a new line
onBrushLineAdded({ onBrushLineAdded({
layerId: selectedLayer.id, 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, color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
}); });
setIsDrawing(true); setIsDrawing(true);
@ -282,7 +282,10 @@ export const setStageEventHandlers = ({
); );
} else { } else {
// Start a new line // 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); 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_RECT_SHAPE_NAME = 'raster_layer.rect_shape';
export const RASTER_LAYER_IMAGE_NAME = 'raster_layer.image'; 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 // Getters for non-singleton layer and object IDs
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`; export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`;

View File

@ -6,8 +6,8 @@ import {
RG_LAYER_OBJECT_GROUP_NAME, RG_LAYER_OBJECT_GROUP_NAME,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects';
import type { Layer } from 'features/controlLayers/store/types'; import type { LayerData } from 'features/controlLayers/store/types';
import { isRegionalGuidanceLayer, isRGOrRasterlayer } from 'features/controlLayers/store/types'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
@ -185,10 +185,10 @@ const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RAST
*/ */
export const updateBboxes = ( export const updateBboxes = (
stage: Konva.Stage, stage: Konva.Stage,
layerStates: Layer[], layerStates: LayerData[],
onBboxChanged: (layerId: string, bbox: IRect | null) => void onBboxChanged: (layerId: string, bbox: IRect | null) => void
): void => { ): void => {
for (const layerState of layerStates.filter(isRGOrRasterlayer)) { for (const layerState of layerStates) {
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`); const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`);
assert(konvaLayer, `Layer ${layerState.id} not found in stage`); assert(konvaLayer, `Layer ${layerState.id} not found in stage`);
// We only need to recalculate the bbox if the layer has changed // 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 { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; 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 { import {
isControlAdapterLayer, isControlAdapterLayer,
isInitialImageLayer, isInitialImageLayer,
isInpaintMaskLayer,
isRasterLayer, isRasterLayer,
isRegionalGuidanceLayer, isRegionalGuidanceLayer,
isRenderableLayer, isRenderableLayer,
@ -34,7 +35,7 @@ import type { ImageDTO } from 'services/api/types';
*/ */
const renderLayers = ( const renderLayers = (
stage: Konva.Stage, stage: Konva.Stage,
layerStates: Layer[], layerStates: LayerData[],
globalMaskLayerOpacity: number, globalMaskLayerOpacity: number,
tool: Tool, tool: Tool,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>, getImageDTO: (imageName: string) => Promise<ImageDTO | null>,
@ -52,15 +53,14 @@ const renderLayers = (
for (const layer of layerStates) { for (const layer of layerStates) {
if (isRegionalGuidanceLayer(layer)) { if (isRegionalGuidanceLayer(layer)) {
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged);
} } else if (isControlAdapterLayer(layer)) {
if (isControlAdapterLayer(layer)) {
renderCALayer(stage, layer, zIndex, getImageDTO); renderCALayer(stage, layer, zIndex, getImageDTO);
} } else if (isInitialImageLayer(layer)) {
if (isInitialImageLayer(layer)) {
renderIILayer(stage, layer, zIndex, getImageDTO); renderIILayer(stage, layer, zIndex, getImageDTO);
} } else if (isRasterLayer(layer)) {
if (isRasterLayer(layer)) {
renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged); renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged);
} else if (isInpaintMaskLayer(layer)) {
//
} }
// IP Adapter layers are not rendered // IP Adapter layers are not rendered
// Increment the z-index for the tool layer // Increment the z-index for the tool layer

View File

@ -5,7 +5,7 @@ import {
LAYER_BBOX_NAME, LAYER_BBOX_NAME,
PREVIEW_GENERATION_BBOX_DUMMY_RECT, PREVIEW_GENERATION_BBOX_DUMMY_RECT,
} from 'features/controlLayers/konva/naming'; } 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 { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import { t } from 'i18next'; import { t } from 'i18next';
import Konva from 'konva'; 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 layerState The layer state for the layer to create the bounding box for
* @param konvaLayer The konva layer to attach the bounding box to * @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({ const rect = new Konva.Rect({
id: getLayerBboxId(layerState.id), id: getLayerBboxId(layerState.id),
name: LAYER_BBOX_NAME, name: LAYER_BBOX_NAME,

View File

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

View File

@ -1,6 +1,7 @@
import { import {
CA_LAYER_NAME, CA_LAYER_NAME,
INITIAL_IMAGE_LAYER_NAME, INITIAL_IMAGE_LAYER_NAME,
INPAINT_MASK_LAYER_NAME,
RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_IMAGE_NAME, RASTER_LAYER_IMAGE_NAME,
@ -98,7 +99,8 @@ export const selectRenderableLayers = (node: Konva.Node): boolean =>
node.name() === RG_LAYER_NAME || node.name() === RG_LAYER_NAME ||
node.name() === CA_LAYER_NAME || node.name() === CA_LAYER_NAME ||
node.name() === INITIAL_IMAGE_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. * 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 { import {
zControlNetConfigV2, zBeginEndStepPct,
zCLIPVisionModelV2,
zControlModeV2,
zId,
zImageWithDims, zImageWithDims,
zIPAdapterConfigV2, zIPMethodV2,
zT2IAdapterConfigV2, zProcessorConfig,
} from 'features/controlLayers/util/controlAdapters'; } from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type { import type {
ParameterHeight, ParameterHeight,
@ -17,7 +21,6 @@ import {
zAutoNegative, zAutoNegative,
zParameterNegativePrompt, zParameterNegativePrompt,
zParameterPositivePrompt, zParameterPositivePrompt,
zParameterStrength,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import type { ImageDTO } from 'services/api/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', message: 'Must have an even number of points',
}); });
const zOLD_VectorMaskLine = z.object({ const zOLD_VectorMaskLine = z.object({
id: z.string(), id: zId,
type: z.literal('vector_mask_line'), type: z.literal('vector_mask_line'),
tool: zDrawingTool, tool: zDrawingTool,
strokeWidth: z.number().min(1), strokeWidth: z.number().min(1),
@ -39,7 +42,7 @@ const zOLD_VectorMaskLine = z.object({
}); });
const zOLD_VectorMaskRect = z.object({ const zOLD_VectorMaskRect = z.object({
id: z.string(), id: zId,
type: z.literal('vector_mask_rect'), type: z.literal('vector_mask_rect'),
x: z.number(), x: z.number(),
y: z.number(), y: z.number(),
@ -52,6 +55,7 @@ const zRgbColor = z.object({
g: z.number().int().min(0).max(255), g: z.number().int().min(0).max(255),
b: 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({ const zRgbaColor = zRgbColor.extend({
a: z.number().min(0).max(1), 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 zOpacity = z.number().gte(0).lte(1);
const zBrushLine = z.object({ const zBrushLine = z.object({
id: z.string(), id: zId,
type: z.literal('brush_line'), type: z.literal('brush_line'),
strokeWidth: z.number().min(1), strokeWidth: z.number().min(1),
points: zPoints, points: zPoints,
@ -70,7 +74,7 @@ const zBrushLine = z.object({
export type BrushLine = z.infer<typeof zBrushLine>; export type BrushLine = z.infer<typeof zBrushLine>;
const zEraserline = z.object({ const zEraserline = z.object({
id: z.string(), id: zId,
type: z.literal('eraser_line'), type: z.literal('eraser_line'),
strokeWidth: z.number().min(1), strokeWidth: z.number().min(1),
points: zPoints, points: zPoints,
@ -78,7 +82,7 @@ const zEraserline = z.object({
export type EraserLine = z.infer<typeof zEraserline>; export type EraserLine = z.infer<typeof zEraserline>;
const zRectShape = z.object({ const zRectShape = z.object({
id: z.string(), id: zId,
type: z.literal('rect_shape'), type: z.literal('rect_shape'),
x: z.number(), x: z.number(),
y: z.number(), y: z.number(),
@ -89,7 +93,7 @@ const zRectShape = z.object({
export type RectShape = z.infer<typeof zRectShape>; export type RectShape = z.infer<typeof zRectShape>;
const zEllipseShape = z.object({ const zEllipseShape = z.object({
id: z.string(), id: zId,
type: z.literal('ellipse_shape'), type: z.literal('ellipse_shape'),
x: z.number(), x: z.number(),
y: z.number(), y: z.number(),
@ -100,7 +104,7 @@ const zEllipseShape = z.object({
export type EllipseShape = z.infer<typeof zEllipseShape>; export type EllipseShape = z.infer<typeof zEllipseShape>;
const zPolygonShape = z.object({ const zPolygonShape = z.object({
id: z.string(), id: zId,
type: z.literal('polygon_shape'), type: z.literal('polygon_shape'),
points: zPoints, points: zPoints,
color: zRgbaColor, color: zRgbaColor,
@ -108,7 +112,7 @@ const zPolygonShape = z.object({
export type PolygonShape = z.infer<typeof zPolygonShape>; export type PolygonShape = z.infer<typeof zPolygonShape>;
const zImageObject = z.object({ const zImageObject = z.object({
id: z.string(), id: zId,
type: z.literal('image'), type: z.literal('image'),
image: zImageWithDims, image: zImageWithDims,
x: z.number(), x: z.number(),
@ -118,7 +122,7 @@ const zImageObject = z.object({
}); });
export type ImageObject = z.infer<typeof zImageObject>; export type ImageObject = z.infer<typeof zImageObject>;
const zAnyLayerObject = z.discriminatedUnion('type', [ const zLayerObject = z.discriminatedUnion('type', [
zImageObject, zImageObject,
zBrushLine, zBrushLine,
zEraserline, zEraserline,
@ -126,13 +130,7 @@ const zAnyLayerObject = z.discriminatedUnion('type', [
zEllipseShape, zEllipseShape,
zPolygonShape, zPolygonShape,
]); ]);
export type AnyLayerObject = z.infer<typeof zAnyLayerObject>; export type LayerObject = z.infer<typeof zLayerObject>;
const zLayerBase = z.object({
id: z.string(),
isEnabled: z.boolean().default(true),
isSelected: z.boolean().default(true),
});
const zRect = z.object({ const zRect = z.object({
x: z.number(), x: z.number(),
@ -140,33 +138,36 @@ const zRect = z.object({
width: z.number().min(1), width: z.number().min(1),
height: 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(), x: z.number(),
y: z.number(), y: z.number(),
bbox: zRect.nullable(), bbox: zRect.nullable(),
bboxNeedsUpdate: z.boolean(), bboxNeedsUpdate: z.boolean(),
});
const zRasterLayer = zRenderableLayerBase.extend({
type: z.literal('raster_layer'),
opacity: zOpacity, 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({ const zIPAdapterData = z.object({
type: z.literal('control_adapter_layer'), id: zId,
opacity: zOpacity, type: z.literal('ip_adapter'),
isFilterEnabled: z.boolean(), isEnabled: z.boolean(),
controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]), 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>; export type IPAdapterData = z.infer<typeof zIPAdapterData>;
export type IPAdapterConfig = Pick<
const zIPAdapterLayer = zLayerBase.extend({ IPAdapterData,
type: z.literal('ip_adapter_layer'), 'weight' | 'image' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method'
ipAdapter: zIPAdapterConfigV2, >;
});
export type IPAdapterLayer = z.infer<typeof zIPAdapterLayer>;
const zMaskObject = z const zMaskObject = z
.discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zBrushLine, zEraserline, zRectShape]) .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zBrushLine, zEraserline, zRectShape])
@ -201,69 +202,109 @@ const zMaskObject = z
}) })
.pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape]));
const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({ const zRegionalGuidanceData = z.object({
type: z.literal('regional_guidance_layer'), id: zId,
maskObjects: z.array(zMaskObject), type: z.literal('regional_guidance'),
positivePrompt: zParameterPositivePrompt.nullable(), isEnabled: z.boolean(),
negativePrompt: zParameterNegativePrompt.nullable(), x: z.number(),
ipAdapters: z.array(zIPAdapterConfigV2), y: z.number(),
previewColor: zRgbColor, bbox: zRect.nullable(),
autoNegative: zAutoNegative, bboxNeedsUpdate: z.boolean(),
uploadedMaskImage: zImageWithDims.nullable(),
});
const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
type: z.literal('regional_guidance_layer'),
objects: z.array(zMaskObject), objects: z.array(zMaskObject),
positivePrompt: zParameterPositivePrompt.nullable(), positivePrompt: zParameterPositivePrompt.nullable(),
negativePrompt: zParameterNegativePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(),
ipAdapters: z.array(zIPAdapterConfigV2), ipAdapters: z.array(zIPAdapterData),
previewColor: zRgbColor, fill: zRgbColor,
autoNegative: zAutoNegative, autoNegative: zAutoNegative,
uploadedMaskImage: zImageWithDims.nullable(), imageCache: zImageWithDims.nullable(),
}); });
// TODO(psyche): This doesn't migrate correctly! export type RegionalGuidanceData = z.infer<typeof zRegionalGuidanceData>;
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>;
const zInitialImageLayer = zRenderableLayerBase.extend({ const zColorFill = z.object({
type: z.literal('initial_image_layer'), 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, opacity: zOpacity,
filter: zFilter,
weight: z.number().gte(-1).lte(2),
image: zImageWithDims.nullable(), 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', [ const zCanvasItemIdentifier = z.object({
zRegionalGuidanceLayer, type: z.enum([
zControlAdapterLayer, zLayerData.shape.type.value,
zIPAdapterLayer, zIPAdapterData.shape.type.value,
zInitialImageLayer, zControlAdapterData.shape.type.value,
zRasterLayer, zRegionalGuidanceData.shape.type.value,
]); zInpaintMaskData.shape.type.value,
export type Layer = z.infer<typeof zLayer>; ]),
id: zId,
});
type CanvasItemIdentifier = z.infer<typeof zCanvasItemIdentifier>;
export type ControlLayersState = { export type CanvasV2State = {
_version: 3; _version: 3;
selectedLayerId: string | null; lastSelectedItem: CanvasItemIdentifier | null;
layers: Layer[]; prompts: {
brushSize: number; positivePrompt: ParameterPositivePrompt;
brushColor: RgbaColor; negativePrompt: ParameterNegativePrompt;
globalMaskLayerOpacity: number; positivePrompt2: ParameterPositiveStylePromptSDXL;
positivePrompt: ParameterPositivePrompt; negativePrompt2: ParameterNegativeStylePromptSDXL;
negativePrompt: ParameterNegativePrompt; shouldConcatPrompts: boolean;
positivePrompt2: ParameterPositiveStylePromptSDXL; };
negativePrompt2: ParameterNegativeStylePromptSDXL; tool: {
shouldConcatPrompts: boolean; selected: Tool;
selectedBuffer: Tool | null;
invertScroll: boolean;
brush: {
width: number;
};
eraser: {
width: number;
};
fill: RgbaColor;
};
size: { size: {
width: ParameterWidth; width: ParameterWidth;
height: ParameterHeight; height: ParameterHeight;
@ -273,45 +314,13 @@ export type ControlLayersState = {
}; };
export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number };
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 AddBrushLineArg = AddEraserLineArg & { color: RgbaColor };
export type AddPointToLineArg = { layerId: string; point: [number, number] }; export type AddPointToLineArg = { id: string; point: [number, number] };
export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor };
export type AddImageObjectArg = { layerId: string; imageDTO: ImageDTO }; export type AddImageObjectArg = { id: string; imageDTO: ImageDTO };
//#region Type guards //#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'; 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'; } from 'services/api/types';
import { z } from 'zod'; import { z } from 'zod';
const zId = z.string().min(1); export const zId = z.string().min(1);
const zCannyProcessorConfig = z.object({ const zCannyProcessorConfig = z.object({
id: zId, id: zId,
@ -120,7 +120,7 @@ const zZoeDepthProcessorConfig = z.object({
}); });
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>; export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
const zProcessorConfig = z.discriminatedUnion('type', [ export const zProcessorConfig = z.discriminatedUnion('type', [
zCannyProcessorConfig, zCannyProcessorConfig,
zColorMapProcessorConfig, zColorMapProcessorConfig,
zContentShuffleProcessorConfig, zContentShuffleProcessorConfig,
@ -145,7 +145,7 @@ export const zImageWithDims = z.object({
}); });
export type ImageWithDims = z.infer<typeof zImageWithDims>; 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)]) .tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
.refine(([begin, end]) => begin < end, { .refine(([begin, end]) => begin < end, {
message: 'Begin must be less than end', message: 'Begin must be less than end',
@ -161,7 +161,7 @@ const zControlAdapterBase = z.object({
beginEndStepPct: zBeginEndStepPct, 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 type ControlModeV2 = z.infer<typeof zControlModeV2>;
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; 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>; 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 type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success; 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 type IPMethodV2 = z.infer<typeof zIPMethodV2>;
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; 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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; 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 { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import { import {
@ -27,7 +27,7 @@ const selectImageUsages = createMemoizedSelector(
selectCanvasSlice, selectCanvasSlice,
selectNodesSlice, selectNodesSlice,
selectControlAdaptersSlice, selectControlAdaptersSlice,
selectControlLayersSlice, selectCanvasV2Slice,
selectImageUsage, selectImageUsage,
], ],
(deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => { (deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => {

View File

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

View File

@ -15,7 +15,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; 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 ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types'; import type { ImageUsage } from 'features/deleteImageModal/store/types';
@ -42,7 +42,7 @@ const DeleteBoardModal = (props: Props) => {
const selectImageUsageSummary = useMemo( const selectImageUsageSummary = useMemo(
() => () =>
createMemoizedSelector( createMemoizedSelector(
[selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice], [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice],
(canvas, nodes, controlAdapters, controlLayers) => { (canvas, nodes, controlAdapters, controlLayers) => {
const allImageUsage = (boardImageNames ?? []).map((imageName) => const allImageUsage = (boardImageNames ?? []).map((imageName) =>
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, 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 { MetadataItemView } from 'features/metadata/components/MetadataItemView';
import type { MetadataHandlers } from 'features/metadata/types'; import type { MetadataHandlers } from 'features/metadata/types';
import { handlers } from 'features/metadata/util/handlers'; import { handlers } from 'features/metadata/util/handlers';
@ -9,7 +9,7 @@ type Props = {
}; };
export const MetadataLayers = ({ metadata }: Props) => { export const MetadataLayers = ({ metadata }: Props) => {
const [layers, setLayers] = useState<Layer[]>([]); const [layers, setLayers] = useState<LayerData[]>([]);
useEffect(() => { useEffect(() => {
const parse = async () => { const parse = async () => {
@ -40,8 +40,8 @@ const MetadataViewLayer = ({
handlers, handlers,
}: { }: {
label: string; label: string;
layer: Layer; layer: LayerData;
handlers: MetadataHandlers<Layer[], Layer>; handlers: MetadataHandlers<LayerData[], LayerData>;
}) => { }) => {
const onRecall = useCallback(() => { const onRecall = useCallback(() => {
if (!handlers.recallItem) { if (!handlers.recallItem) {

View File

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

View File

@ -5,7 +5,7 @@ import {
} from 'features/controlAdapters/util/buildControlAdapter'; } from 'features/controlAdapters/util/buildControlAdapter';
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/konva/naming'; 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 { zLayer } from 'features/controlLayers/store/types';
import { import {
CA_PROCESSOR_DATA, CA_PROCESSOR_DATA,
@ -431,22 +431,22 @@ const parseAllIPAdapters: MetadataParseFunc<IPAdapterConfigMetadata[]> = async (
}; };
//#region Control Layers //#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 // 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 // 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. // example, CL Control Adapters don't support resize mode, so we simply omit that property.
try { try {
const layers: Layer[] = []; const layers: LayerData[] = [];
try { try {
const control_layers = await getProperty(metadata, 'control_layers'); const control_layers = await getProperty(metadata, 'control_layers');
const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); const controlLayersRaw = await getProperty(control_layers, 'layers', isArray);
const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer));
const controlLayers = controlLayersParseResults 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); .map((result) => result.value);
layers.push(...controlLayers); layers.push(...controlLayers);
} catch { } catch {

View File

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

View File

@ -1,5 +1,5 @@
import { getStore } from 'app/store/nanostores/store'; 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 { LoRA } from 'features/lora/store/loraSlice';
import type { import type {
ControlNetConfigMetadata, ControlNetConfigMetadata,
@ -110,7 +110,7 @@ const validateIPAdapters: MetadataValidateFunc<IPAdapterConfigMetadata[]> = (ipA
return new Promise((resolve) => resolve(validatedIPAdapters)); return new Promise((resolve) => resolve(validatedIPAdapters));
}; };
const validateLayer: MetadataValidateFunc<Layer> = async (layer) => { const validateLayer: MetadataValidateFunc<LayerData> = async (layer) => {
if (layer.type === 'control_adapter_layer') { if (layer.type === 'control_adapter_layer') {
const model = layer.controlAdapter.model; const model = layer.controlAdapter.model;
assert(model, 'Control Adapter layer missing model'); assert(model, 'Control Adapter layer missing model');
@ -132,8 +132,8 @@ const validateLayer: MetadataValidateFunc<Layer> = async (layer) => {
return layer; return layer;
}; };
const validateLayers: MetadataValidateFunc<Layer[]> = async (layers) => { const validateLayers: MetadataValidateFunc<LayerData[]> = async (layers) => {
const validatedLayers: Layer[] = []; const validatedLayers: LayerData[] = [];
for (const l of layers) { for (const l of layers) {
try { try {
const validated = await validateLayer(l); 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 { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming';
import { renderers } from 'features/controlLayers/konva/renderers/layers'; import { renderers } from 'features/controlLayers/konva/renderers/layers';
import { rgLayerMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice'; import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice';
import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import type { InitialImageLayer, LayerData, RegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { import {
isControlAdapterLayer, isControlAdapterLayer,
isInitialImageLayer, isInitialImageLayer,
@ -70,7 +70,7 @@ export const addControlLayers = async (
| Invocation<'vae_loader'> | Invocation<'vae_loader'>
| Invocation<'main_model_loader'> | Invocation<'main_model_loader'>
| Invocation<'sdxl_model_loader'> | Invocation<'sdxl_model_loader'>
): Promise<Layer[]> => { ): Promise<LayerData[]> => {
const isSDXL = base === 'sdxl'; const isSDXL = base === 'sdxl';
const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base)); 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; return hasModel && modelMatchesBase && hasImage;
}; };
const isValidLayer = (layer: Layer, base: BaseModelType) => { const isValidLayer = (layer: LayerData, base: BaseModelType) => {
if (!layer.isEnabled) { if (!layer.isEnabled) {
return false; return false;
} }
@ -532,7 +532,7 @@ const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise<I
req.reset(); req.reset();
const imageDTO = await req.unwrap(); const imageDTO = await req.unwrap();
dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); dispatch(regionalGuidanceMaskImageUploaded({ layerId: layer.id, imageDTO }));
return imageDTO; return imageDTO;
}; };
//#endregion //#endregion

View File

@ -2,7 +2,7 @@ import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-a
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; 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 { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
@ -12,7 +12,7 @@ import { useEnqueueBatchMutation } from 'services/api/endpoints/queue';
import { useBoardName } from 'services/api/hooks/useBoardName'; import { useBoardName } from 'services/api/hooks/useBoardName';
const selectPromptsCount = createSelector( const selectPromptsCount = createSelector(
selectControlLayersSlice, selectCanvasV2Slice,
selectDynamicPromptsSlice, selectDynamicPromptsSlice,
(controlLayers, dynamicPrompts) => (controlLayers, dynamicPrompts) =>
getShouldProcessPrompt(controlLayers.present.positivePrompt) ? dynamicPrompts.prompts.length : 1 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 { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; 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 { HrfSettings } from 'features/hrf/components/HrfSettings';
import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
@ -24,7 +24,7 @@ import { ImageSizeCanvas } from './ImageSizeCanvas';
import { ImageSizeLinear } from './ImageSizeLinear'; import { ImageSizeLinear } from './ImageSizeLinear';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
[selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectControlLayersSlice, activeTabNameSelector], [selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector],
(generation, canvas, hrf, controlLayers, activeTabName) => { (generation, canvas, hrf, controlLayers, activeTabName) => {
const { shouldRandomizeSeed, model } = generation; const { shouldRandomizeSeed, model } = generation;
const { hrfEnabled } = hrf; const { hrfEnabled } = hrf;