diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 86daeab55a..cf3088789c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -5,6 +5,7 @@ import type { JSONObject } from 'common/types'; import { bboxHeightChanged, bboxWidthChanged, + controlLayerModelChanged, ipaModelChanged, loraDeleted, modelChanged, @@ -20,6 +21,7 @@ import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; import { + isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig, isLoRAModelConfig, isNonRefinerMainModelConfig, @@ -31,7 +33,7 @@ import { export const addModelsLoadedListener = (startAppListening: AppStartListening) => { startAppListening({ predicate: modelsApi.endpoints.getModelConfigs.matchFulfilled, - effect: async (action, { getState, dispatch }) => { + effect: (action, { getState, dispatch }) => { // models loaded, we need to ensure the selected model is available and if not, select the first one const log = logger('models'); log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`); @@ -169,24 +171,24 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { }; const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { - // const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); - // state.canvasV2.controlAdapters.entities.forEach((ca) => { - // const isModelAvailable = caModels.some((m) => m.key === ca.model?.key); - // if (isModelAvailable) { - // return; - // } - // dispatch(caModelChanged({ id: ca.id, modelConfig: null })); - // }); + const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); + state.canvasV2.controlLayers.entities.forEach((entity) => { + const isModelAvailable = caModels.some((m) => m.key === entity.controlAdapter.model?.key); + if (isModelAvailable) { + return; + } + dispatch(controlLayerModelChanged({ id: entity.id, modelConfig: null })); + }); }; const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { const ipaModels = models.filter(isIPAdapterModelConfig); - state.canvasV2.ipAdapters.entities.forEach(({ id, model }) => { - const isModelAvailable = ipaModels.some((m) => m.key === model?.key); + state.canvasV2.ipAdapters.entities.forEach((entity) => { + const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key); if (isModelAvailable) { return; } - dispatch(ipaModelChanged({ id, modelConfig: null })); + dispatch(ipaModelChanged({ id: entity.id, modelConfig: null })); }); state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 5b18c45d83..eb2ff5bc27 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -154,24 +154,24 @@ const createSelector = (templates: Templates) => }); canvasV2.ipAdapters.entities - .filter((ipa) => ipa.isEnabled) - .forEach((ipa, i) => { + .filter((entity) => entity.isEnabled) + .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ipa.type]); + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; // Must have model - if (!ipa.model) { + if (!entity.ipAdapter.model) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); } // Model base must match - if (ipa.model?.base !== model?.base) { + if (entity.ipAdapter.model?.base !== model?.base) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } // Must have an image - if (!ipa.imageObject) { + if (!entity.ipAdapter.image) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } @@ -182,22 +182,22 @@ const createSelector = (templates: Templates) => }); canvasV2.regions.entities - .filter((rg) => rg.isEnabled) - .forEach((rg, i) => { + .filter((entity) => entity.isEnabled) + .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[rg.type]); + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; // Must have a region - if (rg.objects.length === 0) { + if (entity.objects.length === 0) { problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); } // Must have at least 1 prompt or IP Adapter - if (rg.positivePrompt === null && rg.negativePrompt === null && rg.ipAdapters.length === 0) { + if (entity.positivePrompt === null && entity.negativePrompt === null && entity.ipAdapters.length === 0) { problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); } - rg.ipAdapters.forEach((ipAdapter) => { + entity.ipAdapters.forEach((ipAdapter) => { // Must have model if (!ipAdapter.model) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); @@ -207,7 +207,7 @@ const createSelector = (templates: Templates) => problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } // Must have an image - if (!ipAdapter.imageObject) { + if (!ipAdapter.image) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } }); @@ -219,12 +219,11 @@ const createSelector = (templates: Templates) => }); canvasV2.rasterLayers.entities - .filter((l) => l.isEnabled) - .filter((l) => l.type === 'raster_layer') - .forEach((l, i) => { + .filter((entity) => entity.isEnabled) + .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 433faf4c69..b33f5304fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -21,7 +21,7 @@ export const AddLayerButton = memo(() => { dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } })); }, [defaultControlAdapter, dispatch]); const addIPAdapter = useCallback(() => { - dispatch(ipaAdded({ config: defaultIPAdapter })); + dispatch(ipaAdded({ ipAdapter: defaultIPAdapter })); }, [defaultIPAdapter, dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index 0781f72d08..60b5832063 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -1,4 +1,5 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -17,14 +18,14 @@ type Props = { export const ControlLayer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'control_layer' }), [id]); - const editing = useDisclosure({ defaultIsOpen: false }); + const editing = useBoolean(false); return ( - + - {editing.isOpen ? : } + {editing.isTrue ? : } 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 f71d31dbf0..f775bf9953 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx @@ -1,9 +1,11 @@ import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; 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 { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -15,13 +17,14 @@ type Props = { export const IPAdapter = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'ip_adapter' }), [id]); + const editing = useBoolean(false); return ( - + - + {editing.isTrue ? : } 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 30a7799cd1..d0d91646e9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx @@ -13,7 +13,7 @@ import { ipaModelChanged, ipaWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; +import { selectIPAdapterEntityOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { IPAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -25,7 +25,7 @@ import { IPAdapterModel } from './IPAdapterModel'; export const IPAdapterSettings = memo(() => { const dispatch = useAppDispatch(); const { id } = useEntityIdentifierContext(); - const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id)); + const ipAdapter = useAppSelector((s) => selectIPAdapterEntityOrThrow(s.canvasV2, id).ipAdapter); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { @@ -93,9 +93,9 @@ export const IPAdapterSettings = memo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 11008749db..50be5258e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -1,4 +1,5 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; 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,14 +16,14 @@ type Props = { export const RasterLayer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'raster_layer' }), [id]); - const editing = useDisclosure({ defaultIsOpen: false }); + const editing = useBoolean(false); return ( - + - {editing.isOpen ? : } + {editing.isTrue ? : } 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 3da03db049..40a9ac2c8d 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,5 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; 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,14 +21,14 @@ type Props = { export const RegionalGuidance = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); - const editing = useDisclosure({ defaultIsOpen: false }); + const editing = useBoolean(false); return ( - + - {editing.isOpen ? : } + {editing.isTrue ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx index 541a2a25e3..e8edc65531 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx @@ -1,14 +1,14 @@ import { Badge } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceBadges = memo(() => { const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); + const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx index ed8f4b5804..47497848bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx @@ -14,7 +14,7 @@ import { rgIPAdapterModelChanged, rgIPAdapterWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { RGIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -34,7 +34,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdap dispatch(rgIPAdapterDeleted({ id, ipAdapterId })); }, [dispatch, ipAdapterId, id]); const ipAdapter = useAppSelector((s) => { - const ipa = selectRGOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); + const ipa = selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`); return ipa; }); @@ -123,7 +123,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdap { - const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.map(({ id }) => id)); + const selectIPAdapterIds = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const ipAdapterIds = selectRegionalGuidanceEntityOrThrow(canvasV2, id).ipAdapters.map(({ id }) => id); + if (ipAdapterIds.length === 0) { + return EMPTY_ARRAY; + } + return ipAdapterIds; + }), + [id] + ); + + const ipAdapterIds = useAppSelector(selectIPAdapterIds); if (ipAdapterIds.length === 0) { return null; @@ -17,15 +32,11 @@ export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => { return ( <> - {ipAdapterIds.map((id, index) => ( - - {index > 0 && ( - - - - )} - - + {ipAdapterIds.map((ipAdapterId, index) => ( + + {index > 0 && } + + ))} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index da26f8940c..66e19c9b35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).fill); + const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); const onChange = useCallback( (fill: RgbColor) => { dispatch(rgFillChanged({ id: entityIdentifier.id, fill })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx index 66bcee04e9..bda94d91de 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -37,9 +37,7 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); }, [dispatch, id]); const addIPAdapter = useCallback(() => { - dispatch( - rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid(), type: 'ip_adapter', isEnabled: true } }) - ); + dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid() } })); }, [defaultIPAdapter, dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx index b35f2ece44..f24dedce03 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -15,7 +15,7 @@ type Props = { }; export const RegionalGuidanceNegativePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt ?? ''); + const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx index cd04eee31d..44a371873e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -15,7 +15,7 @@ type Props = { }; export const RegionalGuidancePositivePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt ?? ''); + const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); 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 49c10f120c..fb359e06c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -1,8 +1,9 @@ +import { Divider } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters'; @@ -11,15 +12,31 @@ import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt export const RegionalGuidanceSettings = memo(() => { const { id } = useEntityIdentifierContext(); - const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null); - const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null); - const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); + const hasPositivePrompt = useAppSelector( + (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt !== null + ); + const hasNegativePrompt = useAppSelector( + (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt !== null + ); + const hasIPAdapters = useAppSelector( + (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.length > 0 + ); return ( {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } + {hasPositivePrompt && ( + <> + + {(hasNegativePrompt || hasIPAdapters) && } + + )} + {hasNegativePrompt && ( + <> + + {hasIPAdapters && } + + )} {hasIPAdapters && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx index 0565026cfd..c67fdbe175 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx @@ -14,7 +14,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -24,7 +24,7 @@ export const RegionalGuidanceSettingsPopover = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).autoNegative); + const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).autoNegative); const onChange = useCallback( (e: ChangeEvent) => { dispatch(rgAutoNegativeChanged({ id: entityIdentifier.id, autoNegative: e.target.checked ? 'invert' : 'off' })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx index 0cc621ef63..8021356875 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx @@ -49,7 +49,15 @@ export const CanvasEntityTitleEdit = memo(({ onStopEditing }: Props) => { }, []); return ( - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index e5723d8878..38058f8215 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -38,7 +38,7 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { } else if (entityIdentifier.type === 'raster_layer') { parts.push(t('controlLayers.rasterLayer')); } else if (entityIdentifier.type === 'ip_adapter') { - parts.push(t('controlLayers.ipAdapter')); + parts.push(t('common.ipAdapter')); } else if (entityIdentifier.type === 'regional_guidance') { parts.push(t('controlLayers.regionalGuidance')); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index cc174e8771..5f7aca0237 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -3,7 +3,12 @@ import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectControlLayerOrThrow } from 'features/controlLayers/store/controlLayersReducers'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import type { + CanvasEntityIdentifier, + ControlNetConfig, + IPAdapterConfig, + T2IAdapterConfig, +} from 'features/controlLayers/store/types'; import { initialControlNetV2, initialIPAdapterV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { useMemo } from 'react'; @@ -22,7 +27,7 @@ export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIden return controlAdapter; }; -export const useDefaultControlAdapter = () => { +export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig => { const [modelConfigs] = useControlNetAndT2IAdapterModels(); const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); @@ -43,7 +48,7 @@ export const useDefaultControlAdapter = () => { return defaultControlAdapter; }; -export const useDefaultIPAdapter = () => { +export const useDefaultIPAdapter = (): IPAdapterConfig => { const [modelConfigs] = useIPAdapterModels(); const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts index f718b78a61..e2821b74f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts @@ -38,8 +38,8 @@ export class CanvasFilter { const { config } = this.manager.stateApi.getFilterState(); this.log.trace({ config }, 'Previewing filter'); const dispatch = this.manager.stateApi._store.dispatch; - - const imageDTO = await this.parent.renderer.rasterize(); + const rect = this.parent.transformer.getRelativeRect() + const imageDTO = await this.parent.renderer.rasterize(rect, false); // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now const filterNode = IMAGE_FILTERS[config.type].buildNode(imageDTO, config as never); const enqueueBatchArg: BatchConfig = { @@ -106,6 +106,7 @@ export class CanvasFilter { width: this.imageState.image.height, height: this.imageState.image.width, }, + replaceObjects: true, }); this.parent.renderer.showObjects(); this.manager.stateApi.$filteringEntity.set(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index eb8f62cee8..3df53ee23c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -20,7 +20,6 @@ import type { ImageCache, Rect, } from 'features/controlLayers/store/types'; -import { isValidLayerWithoutControlAdapter } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import { clamp, isEqual } from 'lodash-es'; import { atom } from 'nanostores'; @@ -538,7 +537,8 @@ export class CanvasManager { stageClone.x(0); stageClone.y(0); - const validLayers = layersState.entities.filter(isValidLayerWithoutControlAdapter); + const validLayers = layersState.entities.filter((entity) => entity.isEnabled && entity.objects.length > 0); + // getLayers() returns the internal `children` array of the stage directly - calling destroy on a layer will // mutate that array. We need to clone the array to avoid mutating the original. for (const konvaLayer of stageClone.getLayers().slice()) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 726ec21cf5..e73a83aef7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -374,8 +374,7 @@ export class CanvasObjectRenderer { * @param rect The rect to rasterize. If omitted, the entity's full rect will be used. * @returns A promise that resolves to the rasterized image DTO. */ - rasterize = async (rect?: Rect): Promise => { - rect = rect ?? this.parent.transformer.getRelativeRect(); + rasterize = async (rect: Rect, replaceObjects: boolean = false): Promise => { let imageDTO: ImageDTO | null = null; const rasterizedImageCache = this.getRasterizedImageCache(rect); @@ -400,6 +399,7 @@ export class CanvasObjectRenderer { entityIdentifier: this.parent.getEntityIdentifier(), imageObject, rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: imageDTO.width, height: imageDTO.height }, + replaceObjects, }); return imageDTO; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 7b71014c35..aec02d3e7f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -58,7 +58,7 @@ export class CanvasTransformer { /** * Whether the transformer is currently calculating the rect of the parent. */ - isPendingRectCalculation: boolean = false; + isPendingRectCalculation: boolean = true; /** * A set of subscriptions that should be cleaned up when the transformer is destroyed. @@ -506,7 +506,8 @@ export class CanvasTransformer { */ applyTransform = async () => { this.log.debug('Applying transform'); - await this.parent.renderer.rasterize(); + const rect = this.getRelativeRect(); + await this.parent.renderer.rasterize(rect, true); this.requestRectCalculation(); this.stopTransform(); }; @@ -589,7 +590,7 @@ export class CanvasTransformer { }; updateBbox = () => { - this.log.trace('Updating bbox'); + this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect }, 'Updating bbox'); if (this.isPendingRectCalculation) { this.syncInteractionState(); @@ -600,10 +601,8 @@ export class CanvasTransformer { // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.pixelRect.width === 0 || this.pixelRect.height === 0) { // We shouldn't reset on the first render - the bbox will be calculated on the next render - if (!this.parent.renderer.hasObjects()) { - // The layer is fully transparent but has objects - reset it - this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); - } + // The layer is fully transparent but has objects - reset it + this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); this.syncInteractionState(); return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 450b850c4e..8624e46741 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -154,6 +154,8 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde return state.inpaintMask; case 'regional_guidance': return state.regions.entities.find((rg) => rg.id === id); + case 'ip_adapter': + return state.ipAdapters.entities.find((ipa) => ipa.id === id); default: return; } @@ -246,19 +248,22 @@ export const canvasV2Slice = createSlice({ } }, entityRasterized: (state, action: PayloadAction) => { - const { entityIdentifier, imageObject, rect } = action.payload; + const { entityIdentifier, imageObject, rect, replaceObjects } = action.payload; const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } 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 // rect twice. Just in case, we remove the old cache. entity.rasterizationCache = entity.rasterizationCache.filter((cache) => !isEqual(cache.rect, rect)); entity.rasterizationCache.push({ imageName: imageObject.image.image_name, rect }); + + if (replaceObjects) { + entity.objects = [imageObject]; + entity.position = { x: rect.x, y: rect.y }; + } } }, entityBrushLineAdded: (state, action: PayloadAction) => { @@ -328,6 +333,13 @@ export const canvasV2Slice = createSlice({ if (region) { selectedEntityIdentifier = { type: region.type, id: region.id }; } + } else if (entityIdentifier.type === 'ip_adapter') { + const index = state.ipAdapters.entities.findIndex((layer) => layer.id === entityIdentifier.id); + state.ipAdapters.entities = state.ipAdapters.entities.filter((rg) => rg.id !== entityIdentifier.id); + const entity = state.ipAdapters.entities[index]; + if (entity) { + selectedEntityIdentifier = { type: entity.type, id: entity.id }; + } } else { assert(false, 'Not implemented'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 561a769880..cf0081a17a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -4,30 +4,32 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, CanvasIPAdapterState, IPMethodV2 } from './types'; -import { imageDTOToImageObject } from './types'; +import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPMethodV2 } from './types'; +import { imageDTOToImageWithDims } from './types'; -export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); -export const selectIPAOrThrow = (state: CanvasV2State, id: string) => { - const ipa = selectIPA(state, id); - assert(ipa, `IP Adapter with id ${id} not found`); - return ipa; +export const selectIPAdapterEntity = (state: CanvasV2State, id: string) => + state.ipAdapters.entities.find((ipa) => ipa.id === id); +export const selectIPAdapterEntityOrThrow = (state: CanvasV2State, id: string) => { + const entity = selectIPAdapterEntity(state, id); + assert(entity, `IP Adapter with id ${id} not found`); + return entity; }; export const ipAdaptersReducers = { ipaAdded: { - reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { - const { id, config } = action.payload; + reducer: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterConfig }>) => { + const { id, ipAdapter } = action.payload; const layer: CanvasIPAdapterState = { id, type: 'ip_adapter', + name: null, isEnabled: true, - ...config, + ipAdapter, }; state.ipAdapters.entities.push(layer); state.selectedEntityIdentifier = { type: 'ip_adapter', id }; }, - prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), + prepare: (payload: { ipAdapter: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), }, ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => { const { data } = action.payload; @@ -36,7 +38,7 @@ export const ipAdaptersReducers = { }, ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const ipa = selectIPA(state, id); + const ipa = selectIPAdapterEntity(state, id); if (ipa) { ipa.isEnabled = !ipa.isEnabled; } @@ -49,64 +51,54 @@ export const ipAdaptersReducers = { state.ipAdapters.entities = []; }, ipaImageChanged: { - reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { - const { id, imageDTO, objectId } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null; + entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { const { id, method } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.method = method; + entity.ipAdapter.method = method; }, - ipaModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { + ipaModelChanged: (state, action: PayloadAction<{ id: string; modelConfig: IPAdapterModelConfig | null }>) => { const { id, modelConfig } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - if (modelConfig) { - ipa.model = zModelIdentifierField.parse(modelConfig); - } else { - ipa.model = null; - } + entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null; }, ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { const { id, clipVisionModel } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.clipVisionModel = clipVisionModel; + entity.ipAdapter.clipVisionModel = clipVisionModel; }, ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { const { id, weight } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.weight = weight; + entity.ipAdapter.weight = weight; }, ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { const { id, beginEndStepPct } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.beginEndStepPct = beginEndStepPct; + entity.ipAdapter.beginEndStepPct = beginEndStepPct; }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index cbe65507ba..4aca555381 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,18 +1,32 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject } from 'features/controlLayers/store/types'; +import type { + CanvasV2State, + CLIPVisionModelV2, + IPMethodV2, + RegionalGuidanceIPAdapterConfig, +} from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import { isEqual } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasIPAdapterState, CanvasRegionalGuidanceState, RgbColor } from './types'; +import type { CanvasRegionalGuidanceState, RgbColor } from './types'; -export const selectRG = (state: CanvasV2State, id: string) => state.regions.entities.find((rg) => rg.id === id); -export const selectRGOrThrow = (state: CanvasV2State, id: string) => { - const rg = selectRG(state, id); +export const selectRegionalGuidanceEntity = (state: CanvasV2State, id: string) => { + return state.regions.entities.find((rg) => rg.id === id); +}; +export const selectRegionalGuidanceIPAdapter = (state: CanvasV2State, id: string, ipAdapterId: string) => { + const entity = state.regions.entities.find((rg) => rg.id === id); + if (!entity) { + return; + } + return entity.ipAdapters.find((ipa) => ipa.id === ipAdapterId); +}; +export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: string) => { + const rg = selectRegionalGuidanceEntity(state, id); assert(rg, `Region with id ${id} not found`); return rg; }; @@ -72,105 +86,89 @@ export const regionsReducers = { }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.positivePrompt = prompt; + entity.positivePrompt = prompt; }, rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.negativePrompt = prompt; + entity.negativePrompt = prompt; }, rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { const { id, fill } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.fill = fill; + entity.fill = fill; }, rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { const { id, autoNegative } = action.payload; - const rg = selectRG(state, id); + const rg = selectRegionalGuidanceEntity(state, id); if (!rg) { return; } rg.autoNegative = autoNegative; }, - rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: CanvasIPAdapterState }>) => { + rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: RegionalGuidanceIPAdapterConfig }>) => { const { id, ipAdapter } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.ipAdapters.push(ipAdapter); + entity.ipAdapters.push(ipAdapter); }, rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { const { id, ipAdapterId } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + entity.ipAdapters = entity.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, rgIPAdapterImageChanged: ( state, - action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null; objectId: string }> + action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> ) => { const { id, ipAdapterId, imageDTO } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null; + ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { const { id, ipAdapterId, weight } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.weight = weight; + ipAdapter.weight = weight; }, rgIPAdapterBeginEndStepPctChanged: ( state, action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> ) => { const { id, ipAdapterId, beginEndStepPct } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.beginEndStepPct = beginEndStepPct; + ipAdapter.beginEndStepPct = beginEndStepPct; }, rgIPAdapterMethodChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>) => { const { id, ipAdapterId, method } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.method = method; + ipAdapter.method = method; }, rgIPAdapterModelChanged: ( state, @@ -181,33 +179,21 @@ export const regionsReducers = { }> ) => { const { id, ipAdapterId, modelConfig } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - if (modelConfig) { - ipa.model = zModelIdentifierField.parse(modelConfig); - } else { - ipa.model = null; - } + ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null; }, rgIPAdapterCLIPVisionModelChanged: ( state, action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { id, ipAdapterId, clipVisionModel } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.clipVisionModel = clipVisionModel; + ipAdapter.clipVisionModel = clipVisionModel; }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index cc0fb37f35..c7e54b2a83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -581,22 +581,24 @@ export function isCanvasBrushLineState(obj: CanvasObjectState): obj is CanvasBru return obj.type === 'brush_line'; } +const zIPAdapterConfig = z.object({ + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + weight: z.number().gte(-1).lte(2), + beginEndStepPct: zBeginEndStepPct, + method: zIPMethodV2, + clipVisionModel: zCLIPVisionModelV2, +}); +export type IPAdapterConfig = z.infer; + export const zCanvasIPAdapterState = z.object({ id: zId, + name: z.string().nullable(), type: z.literal('ip_adapter'), isEnabled: z.boolean(), - weight: z.number().gte(-1).lte(2), - method: zIPMethodV2, - imageObject: zCanvasImageState.nullable(), - model: zModelIdentifierField.nullable(), - clipVisionModel: zCLIPVisionModelV2, - beginEndStepPct: zBeginEndStepPct, + ipAdapter: zIPAdapterConfig, }); export type CanvasIPAdapterState = z.infer; -export type IPAdapterConfig = Pick< - CanvasIPAdapterState, - 'weight' | 'imageObject' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' ->; const zMaskObject = z .discriminatedUnion('type', [ @@ -645,6 +647,17 @@ const zImageCache = z.object({ }); export type ImageCache = z.infer; +const zRegionalGuidanceIPAdapterConfig = z.object({ + id: zId, + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + weight: z.number().gte(-1).lte(2), + beginEndStepPct: zBeginEndStepPct, + method: zIPMethodV2, + clipVisionModel: zCLIPVisionModelV2, +}); +export type RegionalGuidanceIPAdapterConfig = z.infer; + export const zCanvasRegionalGuidanceState = z.object({ id: zId, name: z.string().nullable(), @@ -655,7 +668,7 @@ export const zCanvasRegionalGuidanceState = z.object({ fill: zRgbColor, positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zCanvasIPAdapterState), + ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig), autoNegative: zAutoNegative, rasterizationCache: z.array(zImageCache), }); @@ -763,7 +776,7 @@ export const initialT2IAdapterV2: T2IAdapterConfig = { }; export const initialIPAdapterV2: IPAdapterConfig = { - imageObject: null, + image: null, model: null, beginEndStepPct: [0, 1], method: 'full', @@ -943,6 +956,7 @@ export type EntityRasterizedPayload = { entityIdentifier: CanvasEntityIdentifier; imageObject: CanvasImageState; rect: Rect; + replaceObjects: boolean; }; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts index 569b57310d..99d5009220 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -1,4 +1,4 @@ -import type { CanvasIPAdapterState } from 'features/controlLayers/store/types'; +import type { CanvasIPAdapterState, IPAdapterConfig } from 'features/controlLayers/store/types'; import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { BaseModelType, Invocation } from 'services/api/types'; @@ -10,7 +10,7 @@ export const addIPAdapters = ( denoise: Invocation<'denoise_latents'>, base: BaseModelType ): CanvasIPAdapterState[] => { - const validIPAdapters = ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + const validIPAdapters = ipAdapters.filter((entity) => isValidIPAdapter(entity.ipAdapter, base)); for (const ipa of validIPAdapters) { addIPAdapter(ipa, g, denoise); } @@ -33,13 +33,14 @@ export const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise } }; -const addIPAdapter = (ipa: CanvasIPAdapterState, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject } = ipa; - assert(imageObject, 'IP Adapter image is required'); +const addIPAdapter = (entity: CanvasIPAdapterState, g: Graph, denoise: Invocation<'denoise_latents'>) => { + const { id, ipAdapter } = entity; + const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; + assert(image, 'IP Adapter image is required'); assert(model, 'IP Adapter model is required'); const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const ipAdapter = g.addNode({ + const ipAdapterNode = g.addNode({ id: `ip_adapter_${id}`, type: 'ip_adapter', weight, @@ -49,16 +50,16 @@ const addIPAdapter = (ipa: CanvasIPAdapterState, g: Graph, denoise: Invocation<' begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: imageObject.image.image_name, + image_name: image.image_name, }, }); - g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); + g.addEdge(ipAdapterNode, 'ip_adapter', ipAdapterCollect, 'item'); }; -export const isValidIPAdapter = (ipa: CanvasIPAdapterState, base: BaseModelType): boolean => { +export const isValidIPAdapter = (ipAdapter: IPAdapterConfig, base: BaseModelType): boolean => { // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ipa.model); - const modelMatchesBase = ipa.model?.base === base; - const hasImage = Boolean(ipa.imageObject); + const hasModel = Boolean(ipAdapter.model); + const modelMatchesBase = ipAdapter.model?.base === base; + const hasImage = Boolean(ipAdapter.image); return hasModel && modelMatchesBase && hasImage; }; 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 d647eb556f..4b40c94172 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,10 +1,5 @@ import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; -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 - layer.objects.length > 0 && - layer.controlAdapter === null - ); +export const isValidLayer = (layer: CanvasRasterLayerState) => { + return layer.isEnabled && layer.objects.length > 0; }; 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 f4a8f429e6..a0a2b09829 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 @@ -1,6 +1,10 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasIPAdapterState, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; +import type { + CanvasRegionalGuidanceState, + Rect, + RegionalGuidanceIPAdapterConfig, +} from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, PROMPT_REGION_MASK_TO_TENSOR_PREFIX, @@ -174,13 +178,15 @@ export const addRegions = async ( } } - const validRGIPAdapters: CanvasIPAdapterState[] = region.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + const validRGIPAdapters: RegionalGuidanceIPAdapterConfig[] = region.ipAdapters.filter((ipAdapter) => + isValidIPAdapter(ipAdapter, base) + ); for (const ipa of validRGIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject } = ipa; + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipa; assert(model, 'IP Adapter model is required'); - assert(imageObject, 'IP Adapter image is required'); + assert(image, 'IP Adapter image is required'); const ipAdapter = g.addNode({ id: `ip_adapter_${id}`, @@ -192,7 +198,7 @@ export const addRegions = async ( begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: imageObject.image.image_name, + image_name: image.image_name, }, }); 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 ef403a5ae7..4ac130d15e 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.rasterLayers.entities, + state.canvasV2.controlLayers.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 f80d47b5c6..98fadc83e2 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.rasterLayers.entities, + state.canvasV2.controlLayers.entities, g, state.canvasV2.bbox.rect, denoise,