From 1435557d1dcedeab3f55fc4c02e1b7188dc069bb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:35:07 +1000 Subject: [PATCH] feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers --- invokeai/frontend/web/public/locales/en.json | 4 +- .../addCommitStagingAreaImageListener.ts | 8 +- .../listeners/boardAndImagesDeleted.ts | 4 +- .../listeners/imageDeletionListeners.ts | 4 +- .../listeners/imageDropped.ts | 8 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 79 +++++---- .../components/AddLayerButton.tsx | 4 +- .../components/CanvasEntityList.tsx | 6 +- .../components/ControlLayer/ControlLayer.tsx | 39 +++++ .../ControlLayer/ControlLayerActionsMenu.tsx | 17 ++ .../ControlLayerControlAdapter.tsx} | 38 +++-- .../ControlLayer/ControlLayerEntityList.tsx | 38 +++++ .../ControlLayersSettingsPopover.tsx | 21 +++ .../components/ControlLayersToolbar.tsx | 27 +--- .../components/DeleteAllLayersButton.tsx | 2 +- .../components/Filters/Filter.tsx | 6 +- .../components/Filters/FilterWrapper.tsx | 17 -- .../components/IPAdapter/IPAdapter.tsx | 7 +- .../IPAdapter/IPAdapterSettings.tsx | 6 +- .../components/InpaintMask/InpaintMask.tsx | 36 +++-- .../InpaintMask/InpaintMaskSettings.tsx | 8 - .../components/Layer/LayerSettings.tsx | 22 --- .../Layer.tsx => RasterLayer/RasterLayer.tsx} | 12 +- .../RasterLayerActionsMenu.tsx} | 4 +- .../RasterLayerEntityList.tsx} | 14 +- .../RegionalGuidance/RegionalGuidance.tsx | 7 +- .../RegionalGuidanceSettings.tsx | 6 +- .../common/CanvasEntityActionMenuItems.tsx | 59 +++++-- .../common/CanvasEntityGroupTitle.tsx | 2 +- ...gs.tsx => CanvasEntitySettingsWrapper.tsx} | 4 +- .../hooks/useCanvasResetLayerHotkey.ts | 19 +-- .../hooks/useEntityObjectCount.ts | 8 +- .../controlLayers/hooks/useEntityTitle.ts | 8 +- .../hooks/useLayerControlAdapter.ts | 43 ++--- .../controlLayers/konva/CanvasLayerAdapter.ts | 11 +- .../controlLayers/konva/CanvasManager.ts | 131 ++++++++------- .../controlLayers/konva/CanvasStateApi.ts | 43 ++--- .../controlLayers/konva/CanvasTool.ts | 10 +- .../features/controlLayers/konva/events.ts | 9 +- .../controlLayers/store/bboxReducers.ts | 6 +- .../controlLayers/store/canvasV2Slice.ts | 153 +++++++++++------- .../store/controlLayersReducers.ts | 153 ++++++++++++++++++ .../controlLayers/store/layersReducers.ts | 142 ---------------- .../store/rasterLayersReducers.ts | 108 +++++++++++++ .../features/controlLayers/store/selectors.ts | 2 +- .../src/features/controlLayers/store/types.ts | 45 +++--- .../deleteImageModal/store/selectors.ts | 2 +- .../metadata/components/MetadataLayers.tsx | 8 +- .../src/features/metadata/util/handlers.ts | 6 +- .../web/src/features/metadata/util/parsers.ts | 18 +-- .../src/features/metadata/util/recallers.ts | 14 +- .../src/features/metadata/util/validators.ts | 8 +- .../graph/generation/addControlAdapters.ts | 67 ++------ .../nodes/util/graph/generation/addInpaint.ts | 2 +- .../nodes/util/graph/generation/addLayers.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 2 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- 59 files changed, 866 insertions(+), 671 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/LayerControlAdapter.tsx => ControlLayer/ControlLayerControlAdapter.tsx} (62%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/Layer.tsx => RasterLayer/RasterLayer.tsx} (78%) rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/LayerActionsMenu.tsx => RasterLayer/RasterLayerActionsMenu.tsx} (80%) rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/LayerEntityList.tsx => RasterLayer/RasterLayerEntityList.tsx} (68%) rename invokeai/frontend/web/src/features/controlLayers/components/common/{CanvasEntitySettings.tsx => CanvasEntitySettingsWrapper.tsx} (58%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e03fd80d2f..42b7f80fc8 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1674,7 +1674,9 @@ "opacity": "Opacity", "regionalGuidance_withCount": "Regional Guidance ({{count}})", "controlAdapters_withCount": "Control Adapters ({{count}})", - "layers_withCount": "Raster Layers ({{count}})", + "controlLayer": "Control Layer", + "controlLayers_withCount": "Control Layers ({{count}})", + "rasterLayers_withCount": "Raster Layers ({{count}})", "ipAdapters_withCount": "IP Adapters ({{count}})", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 98ed8071cd..d6cb10ff43 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -2,11 +2,11 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { $lastProgressEvent, - layerAdded, + rasterLayerAdded, sessionStagingAreaImageAccepted, sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -62,12 +62,12 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { const { imageDTO, offsetX, offsetY } = stagingAreaImage; const imageObject = imageDTOToImageObject(imageDTO); - const overrides: Partial = { + const overrides: Partial = { position: { x: x + offsetX, y: y + offsetY }, objects: [imageObject], }; - api.dispatch(layerAdded({ overrides })); + api.dispatch(rasterLayerAdded({ overrides })); api.dispatch(sessionStagingAreaReset()); }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index f5c3a95537..82681c6d79 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { ipaAllDeleted, layerAllDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { ipaAllDeleted, rasterLayerAllDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -22,7 +22,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS const imageUsage = getImageUsage(nodes.present, canvasV2, image_name); if (imageUsage.isLayerImage && !wereLayersReset) { - dispatch(layerAllDeleted()); + dispatch(rasterLayerAllDeleted()); wereLayersReset = true; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 17e6596701..2459f2db4f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -55,7 +55,7 @@ const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO }; const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.layers.entities.forEach(({ id, objects }) => { + state.canvasV2.rasterLayers.entities.forEach(({ id, objects }) => { let shouldDelete = false; for (const obj of objects) { if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) { @@ -64,7 +64,7 @@ const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im } } if (shouldDelete) { - dispatch(entityDeleted({ entityIdentifier: { id, type: 'layer' } })); + dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } })); } }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 39bb31ce63..9adb0da5bc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -4,10 +4,10 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { parseify } from 'common/util/serialize'; import { ipaImageChanged, - layerAdded, + rasterLayerAdded, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; @@ -108,11 +108,11 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const imageObject = imageDTOToImageObject(activeData.payload.imageDTO); const { x, y } = getState().canvasV2.bbox.rect; - const overrides: Partial = { + const overrides: Partial = { objects: [imageObject], position: { x, y }, }; - dispatch(layerAdded({ overrides, isSelected: true })); + dispatch(rasterLayerAdded({ overrides, isSelected: true })); return; } diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 48df41fc48..5b18c45d83 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -2,7 +2,6 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntityState } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -18,14 +17,13 @@ import { forEach, upperFirst } from 'lodash-es'; import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -const LAYER_TYPE_TO_TKEY: Record = { - control_adapter: 'controlLayers.globalControlAdapter', - ip_adapter: 'controlLayers.globalIPAdapter', - regional_guidance: 'controlLayers.regionalGuidance', - layer: 'controlLayers.raster', +const LAYER_TYPE_TO_TKEY = { + ip_adapter: 'controlLayers.ipAdapter', inpaint_mask: 'controlLayers.inpaintMask', - initial_image: 'controlLayers.initialImage', -}; + regional_guidance: 'controlLayers.regionalGuidance', + raster_layer: 'controlLayers.raster', + control_layer: 'controlLayers.globalControlAdapter', +} as const; const createSelector = (templates: Templates) => createMemoizedSelector( @@ -125,41 +123,35 @@ const createSelector = (templates: Templates) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - // canvasV2.controlAdapters.entities - // .filter((ca) => ca.isEnabled) - // .forEach((ca, i) => { - // const layerLiteral = i18n.t('controlLayers.layers_one'); - // const layerNumber = i + 1; - // const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]); - // const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; - // const problems: string[] = []; - // // Must have model - // if (!ca.model) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); - // } - // // Model base must match - // if (ca.model?.base !== model?.base) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); - // } - // // Must have a control image OR, if it has a processor, it must have a processed image - // if (!ca.imageObject) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); - // } else if (ca.processorConfig && !ca.processedImageObject) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); - // } - // // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) - // if (ca.adapterType === 't2i_adapter') { - // const multiple = model?.base === 'sdxl' ? 32 : 64; - // if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { - // problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); - // } - // } + canvasV2.controlLayers.entities + .filter((controlLayer) => controlLayer.isEnabled) + .forEach((controlLayer, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; + // Must have model + if (!controlLayer.controlAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); + } + // Model base must match + if (controlLayer.controlAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); + } + // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) + if (controlLayer.controlAdapter.type === 't2i_adapter') { + const multiple = model?.base === 'sdxl' ? 32 : 64; + if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); + } + } - // if (problems.length) { - // const content = upperFirst(problems.join(', ')); - // reasons.push({ prefix, content }); - // } - // }); + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); canvasV2.ipAdapters.entities .filter((ipa) => ipa.isEnabled) @@ -226,8 +218,9 @@ const createSelector = (templates: Templates) => } }); - canvasV2.layers.entities + canvasV2.rasterLayers.entities .filter((l) => l.isEnabled) + .filter((l) => l.type === 'raster_layer') .forEach((l, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 2c4ba2932b..0234f49ab3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { layerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -15,7 +15,7 @@ export const AddLayerButton = memo(() => { dispatch(rgAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { - dispatch(layerAdded({ isSelected: true })); + dispatch(rasterLayerAdded({ isSelected: true })); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index edb1708aae..86d76a4578 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -1,8 +1,9 @@ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList'; -import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; +import { RasterLayerEntityList } from 'features/controlLayers/components/RasterLayer/RasterLayerEntityList'; import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList'; import { memo } from 'react'; @@ -13,7 +14,8 @@ export const CanvasEntityList = memo(() => { - + + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx new file mode 100644 index 0000000000..046d43f46e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -0,0 +1,39 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { ControlLayerActionsMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerActionsMenu'; +import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; + +type Props = { + id: string; +}; + +export const ControlLayer = memo(({ id }: Props) => { + const entityIdentifier = useMemo(() => ({ id, type: 'control_layer' }), [id]); + + return ( + + + + + + + + + + + + + + + ); +}); + +ControlLayer.displayName = 'ControlLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx new file mode 100644 index 0000000000..3f2fa45e06 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx @@ -0,0 +1,17 @@ +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; +import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { memo } from 'react'; + +export const ControlLayerActionsMenu = memo(() => { + return ( + + + + + + + ); +}); + +ControlLayerActionsMenu.displayName = 'ControlLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx similarity index 62% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx index 378a411fb1..3e5d1d7fee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx @@ -5,50 +5,48 @@ import { Weight } from 'features/controlLayers/components/common/Weight'; import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect'; import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useControlLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { - layerControlAdapterBeginEndStepPctChanged, - layerControlAdapterControlModeChanged, - layerControlAdapterModelChanged, - layerControlAdapterWeightChanged, + controlLayerBeginEndStepPctChanged, + controlLayerControlModeChanged, + controlLayerModelChanged, + controlLayerWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import type { ControlModeV2, ControlNetConfig, T2IAdapterConfig } from 'features/controlLayers/store/types'; +import type { ControlModeV2 } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -type Props = { - controlAdapter: ControlNetConfig | T2IAdapterConfig; -}; - -export const LayerControlAdapter = memo(({ controlAdapter }: Props) => { +export const ControlLayerControlAdapter = memo(() => { const dispatch = useAppDispatch(); - const { id } = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext(); + const controlAdapter = useControlLayerControlAdapter(entityIdentifier); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { - dispatch(layerControlAdapterBeginEndStepPctChanged({ id, beginEndStepPct })); + dispatch(controlLayerBeginEndStepPctChanged({ id: entityIdentifier.id, beginEndStepPct })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); const onChangeControlMode = useCallback( (controlMode: ControlModeV2) => { - dispatch(layerControlAdapterControlModeChanged({ id, controlMode })); + dispatch(controlLayerControlModeChanged({ id: entityIdentifier.id, controlMode })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); const onChangeWeight = useCallback( (weight: number) => { - dispatch(layerControlAdapterWeightChanged({ id, weight })); + dispatch(controlLayerWeightChanged({ id: entityIdentifier.id, weight })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); const onChangeModel = useCallback( (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - dispatch(layerControlAdapterModelChanged({ id, modelConfig })); + dispatch(controlLayerModelChanged({ id: entityIdentifier.id, modelConfig })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); return ( @@ -63,4 +61,4 @@ export const LayerControlAdapter = memo(({ controlAdapter }: Props) => { ); }); -LayerControlAdapter.displayName = 'LayerControlAdapter'; +ControlLayerControlAdapter.displayName = 'ControlLayerControlAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx new file mode 100644 index 0000000000..640c7dbf38 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -0,0 +1,38 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; +import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + return canvasV2.controlLayers.entities.map(mapId).reverse(); +}); + +export const ControlLayerEntityList = memo(() => { + const { t } = useTranslation(); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_layer')); + const layerIds = useAppSelector(selectEntityIds); + + if (layerIds.length === 0) { + return null; + } + + if (layerIds.length > 0) { + return ( + <> + + {layerIds.map((id) => ( + + ))} + + ); + } +}); + +ControlLayerEntityList.displayName = 'ControlLayerEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 0527377d6f..11da129a8e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -10,8 +10,10 @@ import { PopoverContent, PopoverTrigger, } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { clipToBboxChanged, invertScrollChanged, @@ -25,6 +27,7 @@ import { RiSettings4Fill } from 'react-icons/ri'; const ControlLayersSettingsPopover = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const canvasManager = useStore($canvasManager); const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox); const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); const onChangeInvertScroll = useCallback( @@ -38,6 +41,21 @@ const ControlLayersSettingsPopover = () => { const invalidateRasterizationCaches = useCallback(() => { dispatch(rasterizationCachesInvalidated()); }, [dispatch]); + const calculateBboxes = useCallback(() => { + if (!canvasManager) { + return; + } + for (const adapter of canvasManager.rasterLayerAdapters.values()) { + adapter.transformer.requestRectCalculation(); + } + for (const adapter of canvasManager.controlLayerAdapters.values()) { + adapter.transformer.requestRectCalculation(); + } + for (const adapter of canvasManager.regionalGuidanceAdapters.values()) { + adapter.transformer.requestRectCalculation(); + } + canvasManager.inpaintMaskAdapter.transformer.requestRectCalculation(); + }, [canvasManager]); return ( @@ -58,6 +76,9 @@ const ControlLayersSettingsPopover = () => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index c22d15ed82..c354fd9a19 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,5 +1,5 @@ /* eslint-disable i18next/no-literal-string */ -import { Button, Flex, Switch } from '@invoke-ai/ui-library'; +import { Flex, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; @@ -12,36 +12,15 @@ import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvas import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { nanoid } from 'features/controlLayers/konva/util'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; -const filter = () => { - const entity = $canvasManager.get()?.stateApi.getSelectedEntity(); - if (!entity || entity.type !== 'layer') { - return; - } - entity.adapter.filter.previewFilter({ - type: 'canny_image_processor', - id: nanoid(), - low_threshold: 50, - high_threshold: 50, - }); -}; - export const ControlLayersToolbar = memo(() => { const tool = useAppSelector((s) => s.canvasV2.tool.selected); const canvasManager = useStore($canvasManager); - const bbox = useCallback(() => { - if (!canvasManager) { - return; - } - for (const l of canvasManager.layers.values()) { - l.transformer.requestRectCalculation(); - } - }, [canvasManager]); + const onChangeDebugging = useCallback( (e: ChangeEvent) => { if (!canvasManager) { @@ -61,7 +40,6 @@ export const ControlLayersToolbar = memo(() => { - @@ -70,7 +48,6 @@ export const ControlLayersToolbar = memo(() => { - debug diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index 765608e6d7..b6dc6b4df0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -13,7 +13,7 @@ export const DeleteAllLayersButton = memo(() => { s.canvasV2.regions.entities.length + // s.canvasV2.controlAdapters.entities.length + s.canvasV2.ipAdapters.entities.length + - s.canvasV2.layers.entities.length + s.canvasV2.rasterLayers.entities.length ); }); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx index 98536318a5..e2052026ab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -18,7 +18,7 @@ export const Filter = memo(() => { return; } const entity = canvasManager.stateApi.getEntity(filteringEntity); - if (!entity || entity.type !== 'layer') { + if (!entity || (entity.type !== 'raster_layer' && entity.type !== 'control_layer')) { return; } entity.adapter.filter.previewFilter(); @@ -33,7 +33,7 @@ export const Filter = memo(() => { return; } const entity = canvasManager.stateApi.getEntity(filteringEntity); - if (!entity || entity.type !== 'layer') { + if (!entity || (entity.type !== 'raster_layer' && entity.type !== 'control_layer')) { return; } entity.adapter.filter.applyFilter(); @@ -48,7 +48,7 @@ export const Filter = memo(() => { return; } const entity = canvasManager.stateApi.getEntity(filteringEntity); - if (!entity || entity.type !== 'layer') { + if (!entity || (entity.type !== 'raster_layer' && entity.type !== 'control_layer')) { return; } entity.adapter.filter.cancelFilter(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx deleted file mode 100644 index 6f20a641cc..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useFilter } from 'features/controlLayers/components/Filters/Filter'; -import type { PropsWithChildren } from 'react'; -import { memo } from 'react'; - -const FilterWrapper = (props: PropsWithChildren) => { - const isPreviewDisabled = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.type !== 'layer'); - const filter = useFilter(); - return ( - - {props.children} - - ); -}; - -export default memo(FilterWrapper); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx index 4b47536179..f71d31dbf0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx @@ -1,4 +1,4 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -15,18 +15,17 @@ type Props = { export const IPAdapter = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'ip_adapter' }), [id]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - + - {isOpen && } + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx index a8d331bde9..30a7799cd1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx @@ -1,7 +1,7 @@ import { Box, Flex } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { Weight } from 'features/controlLayers/components/common/Weight'; import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -73,7 +73,7 @@ export const IPAdapterSettings = memo(() => { const postUploadAction = useMemo(() => ({ type: 'SET_IPA_IMAGE', id }), [id]); return ( - + @@ -102,7 +102,7 @@ export const IPAdapterSettings = memo(() => { - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index be9ebbfd67..04b4f88ee6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -1,32 +1,38 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { InpaintMaskActionsMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu'; -import { InpaintMaskSettings } from 'features/controlLayers/components/InpaintMask/InpaintMaskSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { InpaintMaskMaskFillColorPicker } from './InpaintMaskMaskFillColorPicker'; export const InpaintMask = memo(() => { + const { t } = useTranslation(); const entityIdentifier = useMemo(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask')); + return ( - - - - - - - - - - {isOpen && } - - + <> + + + + + + + + + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx deleted file mode 100644 index d7719694a8..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import { memo } from 'react'; - -export const InpaintMaskSettings = memo(() => { - return PLACEHOLDER; -}); - -InpaintMaskSettings.displayName = 'InpaintMaskSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx deleted file mode 100644 index d29278460b..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import { LayerControlAdapter } from 'features/controlLayers/components/Layer/LayerControlAdapter'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { memo } from 'react'; - -export const LayerSettings = memo(() => { - const entityIdentifier = useEntityIdentifierContext(); - const controlAdapter = useLayerControlAdapter(entityIdentifier); - - if (!controlAdapter) { - return null; - } - - return ( - - - - ); -}); - -LayerSettings.displayName = 'LayerSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx similarity index 78% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index a91f104420..e12ce65b4a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -4,8 +4,7 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; -import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; +import { RasterLayerActionsMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerActionsMenu'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -14,8 +13,8 @@ type Props = { id: string; }; -export const Layer = memo(({ id }: Props) => { - const entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); +export const RasterLayer = memo(({ id }: Props) => { + const entityIdentifier = useMemo(() => ({ id, type: 'raster_layer' }), [id]); return ( @@ -24,13 +23,12 @@ export const Layer = memo(({ id }: Props) => { - + - ); }); -Layer.displayName = 'Layer'; +RasterLayer.displayName = 'RasterLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx similarity index 80% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx index 53a30acc37..576c939ad2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx @@ -3,7 +3,7 @@ import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/c import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { memo } from 'react'; -export const LayerActionsMenu = memo(() => { +export const RasterLayerActionsMenu = memo(() => { return ( @@ -14,4 +14,4 @@ export const LayerActionsMenu = memo(() => { ); }); -LayerActionsMenu.displayName = 'LayerActionsMenu'; +RasterLayerActionsMenu.displayName = 'RasterLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx similarity index 68% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx index 1dae90ba0b..1c2c7448f1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx @@ -1,19 +1,19 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; -import { Layer } from 'features/controlLayers/components/Layer/Layer'; +import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.layers.entities.map(mapId).reverse(); + return canvasV2.rasterLayers.entities.map(mapId).reverse(); }); -export const LayerEntityList = memo(() => { +export const RasterLayerEntityList = memo(() => { const { t } = useTranslation(); - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'layer')); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'raster_layer')); const layerIds = useAppSelector(selectEntityIds); if (layerIds.length === 0) { @@ -24,15 +24,15 @@ export const LayerEntityList = memo(() => { return ( <> {layerIds.map((id) => ( - + ))} ); } }); -LayerEntityList.displayName = 'LayerEntityList'; +RasterLayerEntityList.displayName = 'RasterLayerEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index 68f0a9784b..eeed5126a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -1,4 +1,4 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -20,11 +20,10 @@ type Props = { export const RegionalGuidance = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - + @@ -34,7 +33,7 @@ export const RegionalGuidance = memo(({ id }: Props) => { - {isOpen && } + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index f7f96fd79f..49c10f120c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -1,6 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; @@ -16,12 +16,12 @@ export const RegionalGuidanceSettings = memo(() => { const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); return ( - + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } {hasPositivePrompt && } {hasNegativePrompt && } {hasIPAdapters && } - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx index 0a812ce0c2..1c8b61c01e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx @@ -3,16 +3,18 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useLayerUseAsControl } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { useDefaultControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { $filteringEntity, + controlLayerConvertedToRasterLayer, entityArrangedBackwardOne, entityArrangedForwardOne, entityArrangedToBack, entityArrangedToFront, entityDeleted, entityReset, + rasterLayerConvertedToControlLayer, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; @@ -28,17 +30,21 @@ import { PiQuestionMarkBold, PiStarHalfBold, PiTrashSimpleBold, - PiXBold, } from 'react-icons/pi'; const getIndexAndCount = ( canvasV2: CanvasV2State, { id, type }: CanvasEntityIdentifier ): { index: number; count: number } => { - if (type === 'layer') { + if (type === 'raster_layer') { return { - index: canvasV2.layers.entities.findIndex((entity) => entity.id === id), - count: canvasV2.layers.entities.length, + index: canvasV2.rasterLayers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.rasterLayers.entities.length, + }; + } else if (type === 'control_layer') { + return { + index: canvasV2.controlLayers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.controlLayers.entities.length, }; } else if (type === 'regional_guidance') { return { @@ -58,7 +64,6 @@ export const CanvasEntityActionMenuItems = memo(() => { const canvasManager = useStore($canvasManager); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); - const useAsControl = useLayerUseAsControl(entityIdentifier); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { @@ -76,16 +81,39 @@ export const CanvasEntityActionMenuItems = memo(() => { const validActions = useAppSelector(selectValidActions); const isArrangeable = useMemo( - () => entityIdentifier.type === 'layer' || entityIdentifier.type === 'regional_guidance', + () => + entityIdentifier.type === 'raster_layer' || + entityIdentifier.type === 'control_layer' || + entityIdentifier.type === 'regional_guidance', [entityIdentifier.type] ); const isDeleteable = useMemo( - () => entityIdentifier.type === 'layer' || entityIdentifier.type === 'regional_guidance', + () => + entityIdentifier.type === 'raster_layer' || + entityIdentifier.type === 'control_layer' || + entityIdentifier.type === 'regional_guidance', [entityIdentifier.type] ); - const isFilterable = useMemo(() => entityIdentifier.type === 'layer', [entityIdentifier.type]); - const isUseAsControlable = useMemo(() => entityIdentifier.type === 'layer', [entityIdentifier.type]); + + const isFilterable = useMemo( + () => entityIdentifier.type === 'raster_layer' || entityIdentifier.type === 'control_layer', + [entityIdentifier.type] + ); + + const isRasterLayer = useMemo(() => entityIdentifier.type === 'raster_layer', [entityIdentifier.type]); + + const isControlLayer = useMemo(() => entityIdentifier.type === 'control_layer', [entityIdentifier.type]); + + const defaultControlAdapter = useDefaultControlAdapter(); + + const convertRasterLayerToControlLayer = useCallback(() => { + dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id, controlAdapter: defaultControlAdapter })); + }, [dispatch, defaultControlAdapter, entityIdentifier.id]); + + const convertControlLayerToRasterLayer = useCallback(() => { + dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id })); + }, [dispatch, entityIdentifier.id]); const deleteEntity = useCallback(() => { dispatch(entityDeleted({ entityIdentifier })); @@ -142,9 +170,14 @@ export const CanvasEntityActionMenuItems = memo(() => { {t('common.filter')} )} - {isUseAsControlable && ( - : }> - {useAsControl.hasControlAdapter ? t('common.removeControl') : t('common.useAsControl')} + {isRasterLayer && ( + }> + {t('common.convertToControlLayer')} + + )} + {isControlLayer && ( + }> + {t('common.convertToRasterLayer')} )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx index 69fcefe8c3..9c88b28ed5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx @@ -8,7 +8,7 @@ type Props = { export const CanvasEntityGroupTitle = memo(({ title, isSelected }: Props) => { return ( - + {title} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettingsWrapper.tsx similarity index 58% rename from invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettingsWrapper.tsx index d9665c9f0a..b4ddd01703 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettingsWrapper.tsx @@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import type { PropsWithChildren } from 'react'; import { memo } from 'react'; -export const CanvasEntitySettings = memo(({ children }: PropsWithChildren) => { +export const CanvasEntitySettingsWrapper = memo(({ children }: PropsWithChildren) => { return ( {children} @@ -10,4 +10,4 @@ export const CanvasEntitySettings = memo(({ children }: PropsWithChildren) => { ); }); -CanvasEntitySettings.displayName = 'CanvasEntitySettings'; +CanvasEntitySettingsWrapper.displayName = 'CanvasEntitySettingsWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts index 1a8301f204..f15d087b0f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -1,10 +1,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { - entityReset, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; +import { entityReset, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -17,7 +14,6 @@ export function useCanvasResetLayerHotkey() { useAssertSingleton(useCanvasResetLayerHotkey.name); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const resetSelectedLayer = useCallback(() => { if (selectedEntityIdentifier === null) { @@ -27,16 +23,9 @@ export function useCanvasResetLayerHotkey() { }, [dispatch, selectedEntityIdentifier]); const isResetEnabled = useMemo( - () => - (!isStaging && selectedEntityIdentifier?.type === 'layer') || - selectedEntityIdentifier?.type === 'regional_guidance' || - selectedEntityIdentifier?.type === 'inpaint_mask', - [isStaging, selectedEntityIdentifier?.type] + () => selectedEntityIdentifier?.type === 'inpaint_mask', + [selectedEntityIdentifier?.type] ); - useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [ - isResetEnabled, - isStaging, - resetSelectedLayer, - ]); + useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [isResetEnabled, resetSelectedLayer]); } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts index 4a9f383b4c..53ccc5405c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { type CanvasEntityIdentifier,isDrawableEntity } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) => { @@ -11,11 +11,7 @@ export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) = const entity = selectEntity(canvasV2, entityIdentifier); if (!entity) { return 0; - } else if (entity.type === 'layer') { - return entity.objects.length; - } else if (entity.type === 'inpaint_mask') { - return entity.objects.length; - } else if (entity.type === 'regional_guidance') { + } else if (isDrawableEntity(entity)) { return entity.objects.length; } else { return 0; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index 82b9a457d6..a56c82d14b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -13,10 +13,10 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { const parts: string[] = []; if (entityIdentifier.type === 'inpaint_mask') { parts.push(t('controlLayers.inpaintMask')); - } else if (entityIdentifier.type === 'control_adapter') { - parts.push(t('controlLayers.globalControlAdapter')); - } else if (entityIdentifier.type === 'layer') { - parts.push(t('controlLayers.layer')); + } else if (entityIdentifier.type === 'control_layer') { + parts.push(t('controlLayers.controlLayer')); + } else if (entityIdentifier.type === 'raster_layer') { + parts.push(t('controlLayers.rasterLayer')); } else if (entityIdentifier.type === 'ip_adapter') { parts.push(t('controlLayers.ipAdapter')); } else if (entityIdentifier.type === 'regional_guidance') { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 71bb347022..9c91bca847 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -1,23 +1,19 @@ import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; -import { layerUsedAsControlChanged, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import { selectLayer } from 'features/controlLayers/store/layersReducers'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectControlLayerOrThrow } from 'features/controlLayers/store/controlLayersReducers'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { initialControlNetV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; -import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -export const useLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => { +export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => { const selectControlAdapter = useMemo( () => createMemoizedAppSelector(selectCanvasV2Slice, (canvasV2) => { - const layer = selectLayer(canvasV2, entityIdentifier.id); - if (!layer) { - return null; - } + const layer = selectControlLayerOrThrow(canvasV2, entityIdentifier.id); return layer.controlAdapter; }), [entityIdentifier] @@ -26,32 +22,23 @@ export const useLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) return controlAdapter; }; -export const useLayerUseAsControl = (entityIdentifier: CanvasEntityIdentifier) => { - const dispatch = useAppDispatch(); +export const useDefaultControlAdapter = () => { const [modelConfigs] = useControlNetAndT2IAdapterModels(); const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); - const controlAdapter = useLayerControlAdapter(entityIdentifier); - const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => { - // prefer to use a model that matches the base model + const defaultControlAdapter = useMemo(() => { const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); - return compatibleModels[0] ?? modelConfigs[0] ?? null; - }, [baseModel, modelConfigs]); - - const toggle = useCallback(() => { - if (controlAdapter) { - dispatch(layerUsedAsControlChanged({ id: entityIdentifier.id, controlAdapter: null })); - return; - } - const newControlAdapter = deepClone(model?.type === 't2i_adapter' ? initialT2IAdapterV2 : initialControlNetV2); + const model = compatibleModels[0] ?? modelConfigs[0] ?? null; + const controlAdapter = + model?.type === 't2i_adapter' ? deepClone(initialT2IAdapterV2) : deepClone(initialControlNetV2); if (model) { - newControlAdapter.model = zModelIdentifierField.parse(model); + controlAdapter.model = zModelIdentifierField.parse(model); } - dispatch(layerUsedAsControlChanged({ id: entityIdentifier.id, controlAdapter: newControlAdapter })); - }, [controlAdapter, dispatch, entityIdentifier.id, model]); + return controlAdapter; + }, [baseModel, modelConfigs]); - return { hasControlAdapter: Boolean(controlAdapter), toggle }; + return defaultControlAdapter; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index c2b0d7c51a..82b2317d9e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -4,7 +4,12 @@ import { CanvasFilter } from 'features/controlLayers/konva/CanvasFilter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import type { CanvasEntityIdentifier, CanvasLayerState, CanvasV2State } from 'features/controlLayers/store/types'; +import type { + CanvasControlLayerState, + CanvasEntityIdentifier, + CanvasRasterLayerState, + CanvasV2State, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; @@ -17,7 +22,7 @@ export class CanvasLayerAdapter { manager: CanvasManager; log: Logger; - state: CanvasLayerState; + state: CanvasRasterLayerState | CanvasControlLayerState; konva: { layer: Konva.Layer; @@ -110,7 +115,7 @@ export class CanvasLayerAdapter { this.konva.layer.visible(isEnabled); }; - updateObjects = async (arg?: { objects: CanvasLayerState['objects'] }) => { + updateObjects = async (arg?: { objects: CanvasRasterLayerState['objects'] }) => { this.log.trace('Updating objects'); const objects = get(arg, 'objects', this.state.objects); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 9d1f76ff4d..eb8f62cee8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -29,7 +29,6 @@ import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { CanvasBackground } from './CanvasBackground'; -import type { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; @@ -46,10 +45,10 @@ export class CanvasManager { path: string[]; stage: Konva.Stage; container: HTMLDivElement; - controlAdapters: Map; - layers: Map; - regions: Map; - inpaintMask: CanvasMaskAdapter; + rasterLayerAdapters: Map = new Map(); + controlLayerAdapters: Map = new Map(); + regionalGuidanceAdapters: Map = new Map(); + inpaintMaskAdapter: CanvasMaskAdapter; stateApi: CanvasStateApi; preview: CanvasPreview; background: CanvasBackground; @@ -94,10 +93,6 @@ export class CanvasManager { this.background = new CanvasBackground(this); this.stage.add(this.background.konva.layer); - this.layers = new Map(); - this.regions = new Map(); - this.controlAdapters = new Map(); - this._worker.onmessage = (event: MessageEvent) => { const { type, data } = event.data; if (type === 'log') { @@ -128,8 +123,8 @@ export class CanvasManager { this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); - this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); - this.stage.add(this.inpaintMask.konva.layer); + this.inpaintMaskAdapter = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); + this.stage.add(this.inpaintMaskAdapter.konva.layer); } enableDebugging() { @@ -152,18 +147,24 @@ export class CanvasManager { } arrangeEntities() { - const { getLayersState, getRegionsState } = this.stateApi; - const layers = getLayersState().entities; - const regions = getRegionsState().entities; let zIndex = 0; + this.background.konva.layer.zIndex(++zIndex); - for (const layer of layers) { - this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex); + + for (const layer of this.stateApi.getRasterLayersState().entities) { + this.rasterLayerAdapters.get(layer.id)?.konva.layer.zIndex(++zIndex); } - for (const rg of regions) { - this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); + + for (const layer of this.stateApi.getControlLayersState().entities) { + this.controlLayerAdapters.get(layer.id)?.konva.layer.zIndex(++zIndex); } - this.inpaintMask.konva.layer.zIndex(++zIndex); + + for (const rg of this.stateApi.getRegionsState().entities) { + this.regionalGuidanceAdapters.get(rg.id)?.konva.layer.zIndex(++zIndex); + } + + this.inpaintMaskAdapter.konva.layer.zIndex(++zIndex); + this.preview.getLayer().zIndex(++zIndex); } @@ -215,12 +216,14 @@ export class CanvasManager { const { id, type } = transformingEntity; - if (type === 'layer') { - return this.layers.get(id) ?? null; + if (type === 'raster_layer') { + return this.rasterLayerAdapters.get(id) ?? null; + } else if (type === 'control_layer') { + return this.controlLayerAdapters.get(id) ?? null; } else if (type === 'inpaint_mask') { - return this.inpaintMask; + return this.inpaintMaskAdapter; } else if (type === 'regional_guidance') { - return this.regions.get(id) ?? null; + return this.regionalGuidanceAdapters.get(id) ?? null; } return null; @@ -268,21 +271,46 @@ export class CanvasManager { return; } - if (this._isFirstRender || state.layers.entities !== this._prevState.layers.entities) { - this.log.debug('Rendering layers'); + if (this._isFirstRender || state.rasterLayers.entities !== this._prevState.rasterLayers.entities) { + this.log.debug('Rendering raster layers'); - for (const canvasLayer of this.layers.values()) { - if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) { + for (const canvasLayer of this.rasterLayerAdapters.values()) { + if (!state.rasterLayers.entities.find((l) => l.id === canvasLayer.id)) { await canvasLayer.destroy(); - this.layers.delete(canvasLayer.id); + this.rasterLayerAdapters.delete(canvasLayer.id); } } - for (const entityState of state.layers.entities) { - let adapter = this.layers.get(entityState.id); + for (const entityState of state.rasterLayers.entities) { + let adapter = this.rasterLayerAdapters.get(entityState.id); if (!adapter) { adapter = new CanvasLayerAdapter(entityState, this); - this.layers.set(adapter.id, adapter); + this.rasterLayerAdapters.set(adapter.id, adapter); + this.stage.add(adapter.konva.layer); + } + await adapter.update({ + state: entityState, + toolState: state.tool, + isSelected: state.selectedEntityIdentifier?.id === entityState.id, + }); + } + } + + if (this._isFirstRender || state.controlLayers.entities !== this._prevState.controlLayers.entities) { + this.log.debug('Rendering control layers'); + + for (const canvasLayer of this.controlLayerAdapters.values()) { + if (!state.controlLayers.entities.find((l) => l.id === canvasLayer.id)) { + await canvasLayer.destroy(); + this.controlLayerAdapters.delete(canvasLayer.id); + } + } + + for (const entityState of state.controlLayers.entities) { + let adapter = this.controlLayerAdapters.get(entityState.id); + if (!adapter) { + adapter = new CanvasLayerAdapter(entityState, this); + this.controlLayerAdapters.set(adapter.id, adapter); this.stage.add(adapter.konva.layer); } await adapter.update({ @@ -303,18 +331,18 @@ export class CanvasManager { this.log.debug('Rendering regions'); // Destroy the konva nodes for nonexistent entities - for (const canvasRegion of this.regions.values()) { + for (const canvasRegion of this.regionalGuidanceAdapters.values()) { if (!state.regions.entities.find((rg) => rg.id === canvasRegion.id)) { canvasRegion.destroy(); - this.regions.delete(canvasRegion.id); + this.regionalGuidanceAdapters.delete(canvasRegion.id); } } for (const entityState of state.regions.entities) { - let adapter = this.regions.get(entityState.id); + let adapter = this.regionalGuidanceAdapters.get(entityState.id); if (!adapter) { adapter = new CanvasMaskAdapter(entityState, this); - this.regions.set(adapter.id, adapter); + this.regionalGuidanceAdapters.set(adapter.id, adapter); this.stage.add(adapter.konva.layer); } await adapter.update({ @@ -333,7 +361,7 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering inpaint mask'); - await this.inpaintMask.update({ + await this.inpaintMaskAdapter.update({ state: state.inpaintMask, toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === state.inpaintMask.id, @@ -354,11 +382,6 @@ export class CanvasManager { await this.preview.bbox.render(); } - if (this._isFirstRender || state.layers !== this._prevState.layers || state.regions !== this._prevState.regions) { - // this.log.debug('Updating entity bboxes'); - // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); - } - if (this._isFirstRender || state.session !== this._prevState.session) { this.log.debug('Rendering staging area'); await this.preview.stagingArea.render(); @@ -366,7 +389,7 @@ export class CanvasManager { if ( this._isFirstRender || - state.layers.entities !== this._prevState.layers.entities || + state.rasterLayers.entities !== this._prevState.rasterLayers.entities || state.regions.entities !== this._prevState.regions.entities || state.inpaintMask !== this._prevState.inpaintMask || state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id @@ -402,15 +425,15 @@ export class CanvasManager { return () => { this.log.debug('Cleaning up konva renderer'); - this.inpaintMask.destroy(); - for (const region of this.regions.values()) { - region.destroy(); + this.inpaintMaskAdapter.destroy(); + for (const adapter of this.regionalGuidanceAdapters.values()) { + adapter.destroy(); } - for (const layer of this.layers.values()) { - layer.destroy(); + for (const adapter of this.rasterLayerAdapters.values()) { + adapter.destroy(); } - for (const controlAdapter of this.controlAdapters.values()) { - controlAdapter.destroy(); + for (const adapter of this.controlLayerAdapters.values()) { + adapter.destroy(); } this.background.destroy(); this.preview.destroy(); @@ -507,7 +530,7 @@ export class CanvasManager { } getCompositeLayerStageClone = (): Konva.Stage => { - const layersState = this.stateApi.getLayersState(); + const layersState = this.stateApi.getRasterLayersState(); const stageClone = this.stage.clone(); stageClone.scaleX(1); @@ -536,7 +559,7 @@ export class CanvasManager { }; getCompositeRasterizedImageCache = (rect: Rect): ImageCache | null => { - const layerState = this.stateApi.getLayersState(); + const layerState = this.stateApi.getRasterLayersState(); const imageCache = layerState.compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); return imageCache ?? null; }; @@ -567,11 +590,11 @@ export class CanvasManager { }; getInpaintMaskBlob = (rect?: Rect): Promise => { - return this.inpaintMask.renderer.getBlob(rect); + return this.inpaintMaskAdapter.renderer.getBlob(rect); }; getInpaintMaskImageData = (rect?: Rect): ImageData => { - return this.inpaintMask.renderer.getImageData(rect); + return this.inpaintMaskAdapter.renderer.getImageData(rect); }; getGenerationMode(): GenerationMode { @@ -617,7 +640,7 @@ export class CanvasManager { logDebugInfo() { // eslint-disable-next-line no-console console.log(this); - for (const layer of this.layers.values()) { + for (const layer of this.rasterLayerAdapters.values()) { // eslint-disable-next-line no-console console.log(layer); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 4e20aebbe1..b6bb0e8fe1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -26,14 +26,15 @@ import { entityReset, entitySelected, eraserWidthChanged, - layerCompositeRasterized, + rasterLayerCompositeRasterized, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { + CanvasControlLayerState, CanvasEntityIdentifier, CanvasInpaintMaskState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, CanvasV2State, EntityBrushLineAddedPayload, @@ -53,8 +54,14 @@ import { atom } from 'nanostores'; type EntityStateAndAdapter = | { id: string; - type: CanvasLayerState['type']; - state: CanvasLayerState; + type: CanvasRasterLayerState['type']; + state: CanvasRasterLayerState; + adapter: CanvasLayerAdapter; + } + | { + id: string; + type: CanvasControlLayerState['type']; + state: CanvasControlLayerState; adapter: CanvasLayerAdapter; } | { @@ -63,12 +70,6 @@ type EntityStateAndAdapter = state: CanvasInpaintMaskState; adapter: CanvasMaskAdapter; } - // | { - // id: string; - // type: CanvasControlAdapterState['type']; - // state: CanvasControlAdapterState; - // adapter: CanvasControlAdapter; - // } | { id: string; type: CanvasRegionalGuidanceState['type']; @@ -117,7 +118,7 @@ export class CanvasStateApi { }; compositeLayerRasterized = (arg: { imageName: string; rect: Rect }) => { log.trace(arg, 'Composite layer rasterized'); - this._store.dispatch(layerCompositeRasterized(arg)); + this._store.dispatch(rasterLayerCompositeRasterized(arg)); }; setSelectedEntity = (arg: EntityIdentifierPayload) => { log.trace({ arg }, 'Setting selected entity'); @@ -157,8 +158,11 @@ export class CanvasStateApi { getRegionsState = () => { return this.getState().regions; }; - getLayersState = () => { - return this.getState().layers; + getRasterLayersState = () => { + return this.getState().rasterLayers; + }; + getControlLayersState = () => { + return this.getState().controlLayers; }; getInpaintMaskState = () => { return this.getState().inpaintMask; @@ -185,15 +189,18 @@ export class CanvasStateApi { let entityState: EntityStateAndAdapter['state'] | null = null; let entityAdapter: EntityStateAndAdapter['adapter'] | null = null; - if (identifier.type === 'layer') { - entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.layers.get(identifier.id) ?? null; + if (identifier.type === 'raster_layer') { + entityState = state.rasterLayers.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.rasterLayerAdapters.get(identifier.id) ?? null; + } else if (identifier.type === 'control_layer') { + entityState = state.controlLayers.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.controlLayerAdapters.get(identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.regions.get(identifier.id) ?? null; + entityAdapter = this.manager.regionalGuidanceAdapters.get(identifier.id) ?? null; } else if (identifier.type === 'inpaint_mask') { entityState = state.inpaintMask; - entityAdapter = this.manager.inpaintMask; + entityAdapter = this.manager.inpaintMaskAdapter; } if (entityState && entityAdapter) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 32307fd015..9e4183aa2a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -8,6 +8,7 @@ import { BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util'; +import { isDrawableEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -156,10 +157,7 @@ export class CanvasTool { const tool = toolState.selected; - const isDrawableEntity = - selectedEntity?.state.type === 'regional_guidance' || - selectedEntity?.state.type === 'layer' || - selectedEntity?.state.type === 'inpaint_mask'; + const isDrawable = selectedEntity && isDrawableEntity(selectedEntity.state); // Update the stage's pointer style if (tool === 'view') { @@ -168,7 +166,7 @@ export class CanvasTool { } else if (renderedEntityCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; - } else if (!isDrawableEntity) { + } else if (!isDrawable) { // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; } else if (tool === 'move' || Boolean(this.manager.stateApi.$transformingEntity.get())) { @@ -186,7 +184,7 @@ export class CanvasTool { stage.draggable(tool === 'view'); - if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { + if (!cursorPos || renderedEntityCount === 0 || !isDrawable) { // We can bail early if the mouse isn't over the stage or there are no layers this.konva.group.visible(false); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 5be9f5ae87..96465480c1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -6,8 +6,9 @@ import { offsetCoord, } from 'features/controlLayers/konva/util'; import type { + CanvasControlLayerState, CanvasInpaintMaskState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, CanvasV2State, Coordinate, @@ -84,7 +85,7 @@ const getLastPointOfLine = (points: number[]): Coordinate | null => { }; const getLastPointOfLastLineOfEntity = ( - entity: CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState, + entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState, tool: Tool ): Coordinate | null => { const lastObject = entity.objects[entity.objects.length - 1]; @@ -138,7 +139,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { return e.evt.buttons === 1; } - function getClip(entity: CanvasRegionalGuidanceState | CanvasLayerState | CanvasInpaintMaskState) { + function getClip( + entity: CanvasRegionalGuidanceState | CanvasControlLayerState | CanvasRasterLayerState | CanvasInpaintMaskState + ) { const settings = getSettings(); const bboxRect = getBbox().rect; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index 81f9d2fd90..8ac6d4ea4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -12,12 +12,12 @@ import { pick } from 'lodash-es'; export const bboxReducers = { bboxScaledSizeChanged: (state, action: PayloadAction>) => { - state.layers.imageCache = null; + state.rasterLayers.imageCache = null; state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; }, bboxScaleMethodChanged: (state, action: PayloadAction) => { state.bbox.scaleMethod = action.payload; - state.layers.imageCache = null; + state.rasterLayers.imageCache = null; if (action.payload === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); @@ -27,7 +27,7 @@ export const bboxReducers = { }, bboxChanged: (state, action: PayloadAction) => { state.bbox.rect = action.payload; - state.layers.imageCache = null; + state.rasterLayers.imageCache = null; if (state.bbox.scaleMethod === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 1c69a67a97..cc70237b85 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -5,11 +5,12 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; +import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; -import { layersReducers } from 'features/controlLayers/store/layersReducers'; import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; +import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; @@ -23,9 +24,10 @@ import type { InvocationDenoiseProgressEvent } from 'services/events/types'; import { assert } from 'tsafe'; import type { + CanvasControlLayerState, CanvasEntityIdentifier, CanvasInpaintMaskState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, CanvasV2State, Coordinate, @@ -38,12 +40,13 @@ import type { FilterConfig, StageAttrs, } from './types'; -import { IMAGE_FILTERS, RGBA_RED } from './types'; +import { IMAGE_FILTERS, isDrawableEntity, RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, - layers: { entities: [], compositeRasterizationCache: [] }, + rasterLayers: { entities: [], compositeRasterizationCache: [] }, + controlLayers: { entities: [] }, ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], @@ -143,27 +146,21 @@ const initialState: CanvasV2State = { export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) { switch (type) { - case 'layer': - return state.layers.entities.find((layer) => layer.id === id); + case 'raster_layer': + return state.rasterLayers.entities.find((layer) => layer.id === id); + case 'control_layer': + return state.controlLayers.entities.find((layer) => layer.id === id); case 'inpaint_mask': return state.inpaintMask; case 'regional_guidance': return state.regions.entities.find((rg) => rg.id === id); - case 'ip_adapter': - return state.ipAdapters.entities.find((ip) => ip.id === id); default: return; } } -const invalidateCompositeRasterizationCache = (entity: CanvasLayerState, state: CanvasV2State) => { - if (entity.controlAdapter === null) { - state.layers.compositeRasterizationCache = []; - } -}; - const invalidateRasterizationCaches = ( - entity: CanvasLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState, + entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState, state: CanvasV2State ) => { // TODO(psyche): We can be more efficient and only invalidate caches when the entity's changes intersect with the @@ -176,8 +173,8 @@ const invalidateRasterizationCaches = ( // layer's image data will contribute to the composite layer's image data. // If the layer is used as a control layer, it will not contribute to the composite layer, so we do not need to reset // its cache. - if (entity.type === 'layer') { - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + state.rasterLayers.compositeRasterizationCache = []; } }; @@ -185,7 +182,8 @@ export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, reducers: { - ...layersReducers, + ...rasterLayersReducers, + ...controlLayersReducers, ...ipAdaptersReducers, ...regionsReducers, ...lorasReducers, @@ -205,7 +203,7 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } else if (isDrawableEntity(entity)) { entity.isEnabled = true; entity.objects = []; entity.position = { x: 0, y: 0 }; @@ -229,7 +227,7 @@ export const canvasV2Slice = createSlice({ return; } - if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + if (isDrawableEntity(entity)) { entity.position = position; // When an entity is moved, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -242,7 +240,7 @@ export const canvasV2Slice = createSlice({ return; } - if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + if (isDrawableEntity(entity)) { entity.objects = [imageObject]; entity.position = { x: rect.x, y: rect.y }; // Remove the cache for the given rect. This should never happen, because we should never rasterize the same @@ -258,7 +256,7 @@ export const canvasV2Slice = createSlice({ return; } - if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + if (isDrawableEntity(entity)) { entity.objects.push(brushLine); // When adding a brush line, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -269,7 +267,7 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } else if (isDrawableEntity(entity)) { entity.objects.push(eraserLine); // When adding an eraser line, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -282,7 +280,7 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer') { + } else if (isDrawableEntity(entity)) { entity.objects.push(rect); // When adding an eraser line, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -292,18 +290,37 @@ export const canvasV2Slice = createSlice({ }, entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (entity?.type === 'layer') { - // When a layer is deleted, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); - } - if (entityIdentifier.type === 'layer') { - state.layers.entities = state.layers.entities.filter((layer) => layer.id !== entityIdentifier.id); + + let selectedEntityIdentifier: CanvasEntityIdentifier = { type: state.inpaintMask.type, id: state.inpaintMask.id }; + + if (entityIdentifier.type === 'raster_layer') { + // When deleting a raster layer, we need to invalidate the composite rasterization cache. + const index = state.rasterLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); + state.rasterLayers.compositeRasterizationCache = []; + const nextRasterLayer = state.rasterLayers.entities[index]; + if (nextRasterLayer) { + selectedEntityIdentifier = { type: nextRasterLayer.type, id: nextRasterLayer.id }; + } + } else if (entityIdentifier.type === 'control_layer') { + const index = state.controlLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); + state.controlLayers.entities = state.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); + const nextControlLayer = state.controlLayers.entities[index]; + if (nextControlLayer) { + selectedEntityIdentifier = { type: nextControlLayer.type, id: nextControlLayer.id }; + } } else if (entityIdentifier.type === 'regional_guidance') { + const index = state.regions.entities.findIndex((layer) => layer.id === entityIdentifier.id); state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id); + const region = state.regions.entities[index]; + if (region) { + selectedEntityIdentifier = { type: region.type, id: region.id }; + } } else { assert(false, 'Not implemented'); } + + state.selectedEntityIdentifier = selectedEntityIdentifier; }, entityArrangedForwardOne: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; @@ -311,10 +328,12 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveOneToEnd(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveOneToEnd(state.rasterLayers.entities, entity); + // When arranging a raster layer, we need to invalidate the composite rasterization cache. + state.rasterLayers.compositeRasterizationCache = []; + } else if (entity.type === 'control_layer') { + moveOneToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveOneToEnd(state.regions.entities, entity); } @@ -325,10 +344,12 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveToEnd(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveToEnd(state.rasterLayers.entities, entity); + // When arranging a raster layer, we need to invalidate the composite rasterization cache. + state.rasterLayers.compositeRasterizationCache = []; + } else if (entity.type === 'control_layer') { + moveToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveToEnd(state.regions.entities, entity); } @@ -339,10 +360,11 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveOneToStart(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveOneToStart(state.rasterLayers.entities, entity); + // When arranging a raster layer, we need to invalidate the composite rasterization cache. + } else if (entity.type === 'control_layer') { + moveOneToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveOneToStart(state.regions.entities, entity); } @@ -353,18 +375,19 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveToStart(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveToStart(state.rasterLayers.entities, entity); + state.rasterLayers.compositeRasterizationCache = []; + } else if (entity.type === 'control_layer') { + moveToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveToStart(state.regions.entities, entity); } }, allEntitiesDeleted: (state) => { state.regions.entities = []; - state.layers.entities = []; - state.layers.compositeRasterizationCache = []; + state.rasterLayers.entities = []; + state.rasterLayers.compositeRasterizationCache = []; state.ipAdapters.entities = []; }, filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => { @@ -377,8 +400,8 @@ export const canvasV2Slice = createSlice({ // Invalidate the rasterization caches for all entities. // Layers & composite layer - state.layers.compositeRasterizationCache = []; - for (const layer of state.layers.entities) { + state.rasterLayers.compositeRasterizationCache = []; + for (const layer of state.rasterLayers.entities) { layer.rasterizationCache = []; } @@ -399,7 +422,8 @@ export const canvasV2Slice = createSlice({ state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); state.ipAdapters = deepClone(initialState.ipAdapters); - state.layers = deepClone(initialState.layers); + state.rasterLayers = deepClone(initialState.rasterLayers); + state.controlLayers = deepClone(initialState.controlLayers); state.regions = deepClone(initialState.regions); state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); state.session = deepClone(initialState.session); @@ -445,16 +469,21 @@ export const { bboxAspectRatioIdChanged, bboxDimensionsSwapped, bboxSizeOptimized, - // layers - layerAdded, - layerRecalled, - layerAllDeleted, - layerUsedAsControlChanged, - layerControlAdapterModelChanged, - layerControlAdapterControlModeChanged, - layerControlAdapterWeightChanged, - layerControlAdapterBeginEndStepPctChanged, - layerCompositeRasterized, + // Raster layers + rasterLayerAdded, + rasterLayerRecalled, + rasterLayerAllDeleted, + rasterLayerConvertedToControlLayer, + rasterLayerCompositeRasterized, + // Control layers + controlLayerAdded, + controlLayerRecalled, + controlLayerAllDeleted, + controlLayerConvertedToRasterLayer, + controlLayerModelChanged, + controlLayerControlModeChanged, + controlLayerWeightChanged, + controlLayerBeginEndStepPctChanged, // IP Adapters ipaAdded, ipaRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts new file mode 100644 index 0000000000..5e4ef6f251 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts @@ -0,0 +1,153 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { merge, omit } from 'lodash-es'; +import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +import type { + CanvasControlLayerState, + CanvasRasterLayerState, + CanvasV2State, + ControlModeV2, + ControlNetConfig, + T2IAdapterConfig, +} from './types'; +import { initialControlNetV2 } from './types'; + +export const selectControlLayer = (state: CanvasV2State, id: string) => + state.controlLayers.entities.find((layer) => layer.id === id); +export const selectControlLayerOrThrow = (state: CanvasV2State, id: string) => { + const layer = selectControlLayer(state, id); + assert(layer, `Layer with id ${id} not found`); + return layer; +}; + +export const controlLayersReducers = { + controlLayerAdded: { + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; + const layer: CanvasControlLayerState = { + id, + type: 'control_layer', + isEnabled: true, + objects: [], + opacity: 1, + position: { x: 0, y: 0 }, + rasterizationCache: [], + controlAdapter: deepClone(initialControlNetV2), + }; + merge(layer, overrides); + state.controlLayers.entities.push(layer); + if (isSelected) { + state.selectedEntityIdentifier = { type: 'control_layer', id }; + } + }, + prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ + payload: { ...payload, id: getPrefixedId('control_layer') }, + }), + }, + controlLayerRecalled: (state, action: PayloadAction<{ data: CanvasControlLayerState }>) => { + const { data } = action.payload; + state.controlLayers.entities.push(data); + state.selectedEntityIdentifier = { type: 'control_layer', id: data.id }; + }, + controlLayerAllDeleted: (state) => { + state.controlLayers.entities = []; + }, + controlLayerConvertedToRasterLayer: { + reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => { + const { id, newId } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer) { + return; + } + + // Convert the raster layer to control layer + const rasterLayerState: CanvasRasterLayerState = { + ...omit(deepClone(layer), ['type', 'controlAdapter']), + id: newId, + type: 'raster_layer', + }; + + // Remove the control layer + state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== id); + + // Add the new raster layer + state.rasterLayers.entities.push(rasterLayerState); + + // The composite layer's image data will change when the control layer is converted to raster layer. + state.rasterLayers.compositeRasterizationCache = []; + + state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; + }, + prepare: (payload: { id: string }) => ({ + payload: { ...payload, newId: getPrefixedId('raster_layer') }, + }), + }, + controlLayerModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + if (!modelConfig) { + layer.controlAdapter.model = null; + return; + } + layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); + + // We may need to convert the CA to match the model + if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { + // Converting from T2I Adapter to ControlNet - add `controlMode` + const controlNetConfig: ControlNetConfig = { + ...layer.controlAdapter, + type: 'controlnet', + controlMode: 'balanced', + }; + layer.controlAdapter = controlNetConfig; + } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { + // Converting from ControlNet to T2I Adapter - remove `controlMode` + const { controlMode: _, ...rest } = layer.controlAdapter; + const t2iAdapterConfig: T2IAdapterConfig = { ...rest, type: 't2i_adapter' }; + layer.controlAdapter = t2iAdapterConfig; + } + }, + controlLayerControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { + const { id, controlMode } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { + return; + } + layer.controlAdapter.controlMode = controlMode; + }, + controlLayerWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + layer.controlAdapter.weight = weight; + }, + controlLayerBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }> + ) => { + const { id, beginEndStepPct } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + layer.controlAdapter.beginEndStepPct = beginEndStepPct; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts deleted file mode 100644 index 8348848168..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { isEqual, merge } from 'lodash-es'; -import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - -import type { CanvasLayerState, CanvasV2State, ControlModeV2, ControlNetConfig, Rect, T2IAdapterConfig } from './types'; - -export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); -export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { - const layer = selectLayer(state, id); - assert(layer, `Layer with id ${id} not found`); - return layer; -}; - -export const layersReducers = { - layerAdded: { - reducer: ( - state, - action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> - ) => { - const { id, overrides, isSelected } = action.payload; - const layer: CanvasLayerState = { - id, - type: 'layer', - isEnabled: true, - objects: [], - opacity: 1, - position: { x: 0, y: 0 }, - rasterizationCache: [], - controlAdapter: null, - }; - merge(layer, overrides); - state.layers.entities.push(layer); - if (isSelected) { - state.selectedEntityIdentifier = { type: 'layer', id }; - } - - if (layer.objects.length > 0) { - // This new layer will change the composite layer's image data. Invalidate the cache. - state.layers.compositeRasterizationCache = []; - } - }, - prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ - payload: { ...payload, id: getPrefixedId('layer') }, - }), - }, - layerRecalled: (state, action: PayloadAction<{ data: CanvasLayerState }>) => { - const { data } = action.payload; - state.layers.entities.push(data); - state.selectedEntityIdentifier = { type: 'layer', id: data.id }; - if (data.objects.length > 0) { - // This new layer will change the composite layer's image data. Invalidate the cache. - state.layers.compositeRasterizationCache = []; - } - }, - layerAllDeleted: (state) => { - state.layers.entities = []; - state.layers.compositeRasterizationCache = []; - }, - layerCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => { - state.layers.compositeRasterizationCache = state.layers.compositeRasterizationCache.filter( - (cache) => !isEqual(cache.rect, action.payload.rect) - ); - state.layers.compositeRasterizationCache.push(action.payload); - }, - layerUsedAsControlChanged: ( - state, - action: PayloadAction<{ id: string; controlAdapter: ControlNetConfig | T2IAdapterConfig | null }> - ) => { - const { id, controlAdapter } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.controlAdapter = controlAdapter; - // The composite layer's image data will change when the layer is used as control (or not). Invalidate the cache. - state.layers.compositeRasterizationCache = []; - }, - layerControlAdapterModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; - }> - ) => { - const { id, modelConfig } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter) { - return; - } - if (!modelConfig) { - layer.controlAdapter.model = null; - return; - } - layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); - - // We may need to convert the CA to match the model - if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { - // Converting from T2I Adapter to ControlNet - add `controlMode` - const controlNetConfig: ControlNetConfig = { - ...layer.controlAdapter, - type: 'controlnet', - controlMode: 'balanced', - }; - layer.controlAdapter = controlNetConfig; - } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { - // Converting from ControlNet to T2I Adapter - remove `controlMode` - const { controlMode: _, ...rest } = layer.controlAdapter; - const t2iAdapterConfig: T2IAdapterConfig = { ...rest, type: 't2i_adapter' }; - layer.controlAdapter = t2iAdapterConfig; - } - }, - layerControlAdapterControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { - const { id, controlMode } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { - return; - } - layer.controlAdapter.controlMode = controlMode; - }, - layerControlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter) { - return; - } - layer.controlAdapter.weight = weight; - }, - layerControlAdapterBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }> - ) => { - const { id, beginEndStepPct } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter) { - return; - } - layer.controlAdapter.beginEndStepPct = beginEndStepPct; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts new file mode 100644 index 0000000000..728cfe24fb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -0,0 +1,108 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { isEqual, merge } from 'lodash-es'; +import { assert } from 'tsafe'; + +import type { + CanvasControlLayerState, + CanvasRasterLayerState, + CanvasV2State, + ControlNetConfig, + Rect, + T2IAdapterConfig, +} from './types'; + +export const selectRasterLayer = (state: CanvasV2State, id: string) => + state.rasterLayers.entities.find((layer) => layer.id === id); +export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { + const layer = selectRasterLayer(state, id); + assert(layer, `Layer with id ${id} not found`); + return layer; +}; + +export const rasterLayersReducers = { + rasterLayerAdded: { + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; + const layer: CanvasRasterLayerState = { + id, + type: 'raster_layer', + isEnabled: true, + objects: [], + opacity: 1, + position: { x: 0, y: 0 }, + rasterizationCache: [], + }; + merge(layer, overrides); + state.rasterLayers.entities.push(layer); + if (isSelected) { + state.selectedEntityIdentifier = { type: 'raster_layer', id }; + } + + if (layer.objects.length > 0) { + // This new layer will change the composite layer's image data. Invalidate the cache. + state.rasterLayers.compositeRasterizationCache = []; + } + }, + prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ + payload: { ...payload, id: getPrefixedId('raster_layer') }, + }), + }, + rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => { + const { data } = action.payload; + state.rasterLayers.entities.push(data); + state.selectedEntityIdentifier = { type: 'raster_layer', id: data.id }; + if (data.objects.length > 0) { + // This new layer will change the composite layer's image data. Invalidate the cache. + state.rasterLayers.compositeRasterizationCache = []; + } + }, + rasterLayerAllDeleted: (state) => { + state.rasterLayers.entities = []; + state.rasterLayers.compositeRasterizationCache = []; + }, + rasterLayerCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => { + state.rasterLayers.compositeRasterizationCache = state.rasterLayers.compositeRasterizationCache.filter( + (cache) => !isEqual(cache.rect, action.payload.rect) + ); + state.rasterLayers.compositeRasterizationCache.push(action.payload); + }, + rasterLayerConvertedToControlLayer: { + reducer: ( + state, + action: PayloadAction<{ id: string; newId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }> + ) => { + const { id, newId, controlAdapter } = action.payload; + const layer = selectRasterLayer(state, id); + if (!layer) { + return; + } + + // Convert the raster layer to control layer + const controlLayerState: CanvasControlLayerState = { + ...deepClone(layer), + id: newId, + type: 'control_layer', + controlAdapter, + }; + + // Remove the raster layer + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== id); + + // Add the converted control layer + state.controlLayers.entities.push(controlLayerState); + + // The composite layer's image data will change when the raster layer is converted to control layer. + state.rasterLayers.compositeRasterizationCache = []; + + state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; + }, + prepare: (payload: { id: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }) => ({ + payload: { ...payload, newId: getPrefixedId('control_layer') }, + }), + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index b52aca28ae..c81856b5df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -7,7 +7,7 @@ export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) canvasV2.regions.entities.length + // canvasV2.controlAdapters.entities.length + canvasV2.ipAdapters.entities.length + - canvasV2.layers.entities.length + canvasV2.rasterLayers.entities.length ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a19b694670..9d7bd6fa71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -728,23 +728,22 @@ const zT2IAdapterConfig = z.object({ }); export type T2IAdapterConfig = z.infer; -export const zCanvasLayerState = z.object({ +export const zCanvasRasterLayerState = z.object({ id: zId, - type: z.literal('layer'), + type: z.literal('raster_layer'), isEnabled: z.boolean(), position: zCoordinate, opacity: zOpacity, objects: z.array(zCanvasObjectState), rasterizationCache: z.array(zImageCache), - controlAdapter: z.discriminatedUnion('type', [zControlNetConfig, zT2IAdapterConfig]).nullable(), }); -export type CanvasLayerState = z.infer; -export type CanvasLayerStateWithValidControlNet = Omit & { - controlAdapter: Omit & { model: ControlNetModelConfig }; -}; -export type CanvasLayerStateWithValidT2IAdapter = Omit & { - controlAdapter: Omit & { model: T2IAdapterModelConfig }; -}; +export type CanvasRasterLayerState = z.infer; + +export const zCanvasControlLayerState = zCanvasRasterLayerState.extend({ + type: z.literal('control_layer'), + controlAdapter: z.discriminatedUnion('type', [zControlNetConfig, zT2IAdapterConfig]), +}); +export type CanvasControlLayerState = z.infer; export const initialControlNetV2: ControlNetConfig = { type: 'controlnet', @@ -808,8 +807,8 @@ export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMetho zBoundingBoxScaleMethod.safeParse(v).success; export type CanvasEntityState = - | CanvasLayerState - | CanvasControlAdapterState + | CanvasRasterLayerState + | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState | CanvasIPAdapterState; @@ -832,7 +831,8 @@ export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMask: CanvasInpaintMaskState; - layers: { entities: CanvasLayerState[]; compositeRasterizationCache: ImageCache[] }; + rasterLayers: { entities: CanvasRasterLayerState[]; compositeRasterizationCache: ImageCache[] }; + controlLayers: { entities: CanvasControlLayerState[] }; ipAdapters: { entities: CanvasIPAdapterState[] }; regions: { entities: CanvasRegionalGuidanceState[] }; loras: LoRA[]; @@ -962,10 +962,19 @@ export type RemoveIndexString = { export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; +export function isDrawableEntityType(entityType: CanvasEntityState['type']) { + return ( + entityType === 'raster_layer' || + entityType === 'control_layer' || + entityType === 'regional_guidance' || + entityType === 'inpaint_mask' + ); +} + export function isDrawableEntity( entity: CanvasEntityState -): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { - return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask'; +): entity is CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { + return isDrawableEntityType(entity.type); } export function isDrawableEntityAdapter( @@ -973,9 +982,3 @@ export function isDrawableEntityAdapter( ): adapter is CanvasLayerAdapter | CanvasMaskAdapter { return adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasMaskAdapter; } - -export function isDrawableEntityType( - entityType: CanvasEntityState['type'] -): entityType is 'layer' | 'regional_guidance' | 'inpaint_mask' { - return entityType === 'layer' || entityType === 'regional_guidance' || entityType === 'inpaint_mask'; -} diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 23eec84690..a9423747fe 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -11,7 +11,7 @@ import { some } from 'lodash-es'; import type { ImageUsage } from './types'; export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { - const isLayerImage = canvasV2.layers.entities.some((layer) => + const isLayerImage = canvasV2.rasterLayers.entities.some((layer) => layer.objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name) ); diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx index 740a1a2200..c1b2f85d8a 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx @@ -1,4 +1,4 @@ -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; import type { MetadataHandlers } from 'features/metadata/types'; import { handlers } from 'features/metadata/util/handlers'; @@ -9,7 +9,7 @@ type Props = { }; export const MetadataLayers = ({ metadata }: Props) => { - const [layers, setLayers] = useState([]); + const [layers, setLayers] = useState([]); useEffect(() => { const parse = async () => { @@ -40,8 +40,8 @@ const MetadataViewLayer = ({ handlers, }: { label: string; - layer: CanvasLayerState; - handlers: MetadataHandlers; + layer: CanvasRasterLayerState; + handlers: MetadataHandlers; }) => { const onRecall = useCallback(() => { if (!handlers.recallItem) { diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 887b1fbd2c..6bea34438b 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; import type { AnyControlAdapterConfigMetadata, BuildMetadataHandlers, @@ -48,7 +48,7 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (layer) => { +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { let rendered = t('controlLayers.globalInitialImageLayer'); if (layer.image) { @@ -88,7 +88,7 @@ const renderLayerValue: MetadataRenderValueFunc = async (layer } assert(false, 'Unknown layer type'); }; -const renderLayersValue: MetadataRenderValueFunc = async (layers) => { +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 5ee43d344a..e28399809c 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,6 +1,6 @@ import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/controlLayers/konva/naming'; import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; -import type { CanvasControlAdapterState, CanvasIPAdapterState, CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasControlAdapterState, CanvasIPAdapterState, CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, imageDTOToImageWithDims, @@ -8,7 +8,7 @@ import { initialIPAdapterV2, initialT2IAdapterV2, isFilterType, - zCanvasLayerState, + zCanvasRasterLayerState, } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, @@ -424,22 +424,22 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = async (metadataItem) => zCanvasLayerState.parseAsync(metadataItem); +const parseLayer: MetadataParseFunc = async (metadataItem) => zCanvasRasterLayerState.parseAsync(metadataItem); -const parseLayers: MetadataParseFunc = async (metadata) => { +const parseLayers: MetadataParseFunc = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For // example, CL Control Adapters don't support resize mode, so we simply omit that property. try { - const layers: CanvasLayerState[] = []; + const layers: CanvasRasterLayerState[] = []; try { const control_layers = await getProperty(metadata, 'control_layers'); const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); const controlLayers = controlLayersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlLayers); } catch { @@ -498,16 +498,16 @@ const parseLayers: MetadataParseFunc = async (metadata) => { } }; -const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { +const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { // TODO(psyche): recall denoise strength // const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); const imageName = await getProperty(metadata, 'init_image', isString); const imageDTO = await getImageDTO(imageName); assert(imageDTO, 'ImageDTO is null'); const id = getLayerId(uuidv4()); - const layer: CanvasLayerState = { + const layer: CanvasRasterLayerState = { id, - type: 'layer', + type: 'raster_layer', bbox: null, bboxNeedsUpdate: true, x: 0, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 54196820d4..c1bf41d1f4 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -15,8 +15,8 @@ import { bboxWidthChanged, // caRecalled, ipaRecalled, - layerAllDeleted, - layerRecalled, + rasterLayerAllDeleted, + rasterLayerRecalled, loraAllDeleted, loraRecalled, negativePrompt2Changed, @@ -42,7 +42,7 @@ import { import type { CanvasControlAdapterState, CanvasIPAdapterState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, LoRA, } from 'features/controlLayers/store/types'; @@ -328,7 +328,7 @@ const recallRG: MetadataRecallFunc = async (rg) => }; //#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); const clone = deepClone(layer); const invalidObjects: string[] = []; @@ -355,13 +355,13 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } } clone.id = getRGId(uuidv4()); - dispatch(layerRecalled({ data: clone })); + dispatch(rasterLayerRecalled({ data: clone })); return; }; -const recallLayers: MetadataRecallFunc = (layers) => { +const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); - dispatch(layerAllDeleted()); + dispatch(rasterLayerAllDeleted()); for (const l of layers) { recallLayer(l); } diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 5423a7e359..9d667141a0 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, @@ -109,7 +109,7 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateLayer: MetadataValidateFunc = async (layer) => { +const validateLayer: MetadataValidateFunc = async (layer) => { if (layer.type === 'control_adapter_layer') { const model = layer.controlAdapter.model; assert(model, 'Control Adapter layer missing model'); @@ -131,8 +131,8 @@ const validateLayer: MetadataValidateFunc = async (layer) => { return layer; }; -const validateLayers: MetadataValidateFunc = async (layers) => { - const validatedLayers: CanvasLayerState[] = []; +const validateLayers: MetadataValidateFunc = async (layers) => { + const validatedLayers: CanvasRasterLayerState[] = []; for (const l of layers) { try { const validated = await validateLayer(l); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index 8b8023cca3..7f180b858b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -1,15 +1,10 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { - CanvasLayerState, - CanvasLayerStateWithValidControlNet, - CanvasLayerStateWithValidT2IAdapter, + CanvasControlLayerState, ControlNetConfig, - FilterConfig, - ImageWithDims, Rect, T2IAdapterConfig, } from 'features/controlLayers/store/types'; -import type { ImageField } from 'features/nodes/types/common'; import { CONTROL_NET_COLLECT, T2I_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; @@ -17,18 +12,18 @@ import { assert } from 'tsafe'; export const addControlAdapters = async ( manager: CanvasManager, - layers: CanvasLayerState[], + layers: CanvasControlLayerState[], g: Graph, bbox: Rect, denoise: Invocation<'denoise_latents'>, base: BaseModelType -): Promise<(CanvasLayerStateWithValidControlNet | CanvasLayerStateWithValidT2IAdapter)[]> => { - const layersWithValidControlAdapters = layers +): Promise => { + const validControlLayers = layers .filter((layer) => layer.isEnabled) - .filter((layer) => doesLayerHaveValidControlAdapter(layer, base)); + .filter((layer) => isValidControlAdapter(layer.controlAdapter, base)); - for (const layer of layersWithValidControlAdapters) { - const adapter = manager.layers.get(layer.id); + for (const layer of validControlLayers) { + const adapter = manager.controlLayerAdapters.get(layer.id); assert(adapter, 'Adapter not found'); const imageDTO = await adapter.renderer.rasterize(bbox); if (layer.controlAdapter.type === 'controlnet') { @@ -37,7 +32,7 @@ export const addControlAdapters = async ( await addT2IAdapterToGraph(g, layer, imageDTO, denoise); } } - return layersWithValidControlAdapters; + return validControlLayers; }; const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { @@ -59,12 +54,14 @@ const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten const addControlNetToGraph = ( g: Graph, - layer: CanvasLayerStateWithValidControlNet, + layer: CanvasControlLayerState, imageDTO: ImageDTO, denoise: Invocation<'denoise_latents'> ) => { const { id, controlAdapter } = layer; + assert(controlAdapter.type === 'controlnet'); const { beginEndStepPct, model, weight, controlMode } = controlAdapter; + assert(model !== null); const { image_name } = imageDTO; const controlNetCollect = addControlNetCollectorSafe(g, denoise); @@ -103,12 +100,14 @@ const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten const addT2IAdapterToGraph = ( g: Graph, - layer: CanvasLayerStateWithValidT2IAdapter, + layer: CanvasControlLayerState, imageDTO: ImageDTO, denoise: Invocation<'denoise_latents'> ) => { const { id, controlAdapter } = layer; + assert(controlAdapter.type === 't2i_adapter'); const { beginEndStepPct, model, weight } = controlAdapter; + assert(model !== null); const { image_name } = imageDTO; const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); @@ -127,25 +126,6 @@ const addT2IAdapterToGraph = ( g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); }; -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: FilterConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.image_name, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.image_name, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; - const isValidControlAdapter = (controlAdapter: ControlNetConfig | T2IAdapterConfig, base: BaseModelType): boolean => { // Must be have a model const hasModel = Boolean(controlAdapter.model); @@ -153,22 +133,3 @@ const isValidControlAdapter = (controlAdapter: ControlNetConfig | T2IAdapterConf const modelMatchesBase = controlAdapter.model?.base === base; return hasModel && modelMatchesBase; }; - -const doesLayerHaveValidControlAdapter = ( - layer: CanvasLayerState, - base: BaseModelType -): layer is CanvasLayerStateWithValidControlNet | CanvasLayerStateWithValidT2IAdapter => { - if (!layer.controlAdapter) { - // Must have a control adapter - return false; - } - if (!layer.controlAdapter.model) { - // Control adapter must have a model selected - return false; - } - if (layer.controlAdapter.model.base !== base) { - // Selected model must match current base model - return false; - } - return true; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 5f85274ac5..d36a2b8b17 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -22,7 +22,7 @@ export const addInpaint = async ( denoise.denoising_start = denoising_start; const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect); + const maskImage = await manager.inpaintMaskAdapter.renderer.rasterize(bbox.rect); if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index fad839efad..d647eb556f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -1,6 +1,6 @@ -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; -export const isValidLayerWithoutControlAdapter = (layer: CanvasLayerState) => { +export const isValidLayerWithoutControlAdapter = (layer: CanvasRasterLayerState) => { return ( layer.isEnabled && // Boolean(entity.bbox) && TODO(psyche): Re-enable this check when we have a way to calculate bbox for all layers diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 7e4296cb77..118fb779a2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -23,7 +23,7 @@ export const addOutpaint = async ( denoise.denoising_start = denoising_start; const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect); + const maskImage = await manager.inpaintMaskAdapter.renderer.rasterize(bbox.rect); const infill = getInfill(g, compositing); if (!isEqual(scaledSize, originalSize)) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 847348cfb9..f4a8f429e6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -43,7 +43,7 @@ export const addRegions = async ( const validRegions = regions.filter((rg) => isValidRegion(rg, base)); for (const region of validRegions) { - const adapter = manager.regions.get(region.id); + const adapter = manager.regionalGuidanceAdapters.get(region.id); assert(adapter, 'Adapter not found'); const imageDTO = await adapter.renderer.rasterize(bbox); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 8c9e198d6d..ef403a5ae7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -215,7 +215,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P const _addedCAs = await addControlAdapters( manager, - state.canvasV2.layers.entities, + state.canvasV2.rasterLayers.entities, g, state.canvasV2.bbox.rect, denoise, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 56b4292c1e..f80d47b5c6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -219,7 +219,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): const _addedCAs = await addControlAdapters( manager, - state.canvasV2.layers.entities, + state.canvasV2.rasterLayers.entities, g, state.canvasV2.bbox.rect, denoise,