From 1e92bb4e944c92fbc06c2c2a9858980f741627bc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:02:15 +1000 Subject: [PATCH] fix(ui): ref image defaults to prev ref image's image selection A redux selector is used to get the "default" IP Adapter. The selector uses the model list query result to select an IP Adapter model to be preset by default. The selector is memoized, so if we mutate the returned default IP Adapter state, it mutates the result of the selector for all consumers. For example, the `image` property of the default IP Adapter selector result is `null`. When we set the `image` property of the selector result while creating an IP Adapter, this does not trigger the selector to recompute its result. We end up setting the image for the selector result directly, and all other consumers now have that same image set. Solution - we need to clone the selector result everywhere it is used. This was missed in a few spots, causing the issue. --- .../controlLayers/hooks/addLayerHooks.ts | 21 +++++++++++++++---- .../web/src/features/imageActions/actions.ts | 8 +++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 6d6b854eab3..37fe857e15d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -29,7 +29,13 @@ import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/ import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types'; -/** @knipignore */ +/** + * Selects the default control adapter configuration based on the model configurations and the base. + * + * Be sure to clone the output of this selector before modifying it! + * + * @knipignore + */ export const selectDefaultControlAdapter = createSelector( selectModelConfigsQuery, selectBase, @@ -52,6 +58,11 @@ export const selectDefaultControlAdapter = createSelector( } ); +/** + * Selects the default IP adapter configuration based on the model configurations and the base. + * + * Be sure to clone the output of this selector before modifying it! + */ export const selectDefaultIPAdapter = createSelector( selectModelConfigsQuery, selectBase, @@ -117,7 +128,9 @@ export const useAddRegionalReferenceImage = () => { const func = useCallback(() => { const overrides: Partial = { - referenceImages: [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter: defaultIPAdapter }], + referenceImages: [ + { id: getPrefixedId('regional_guidance_reference_image'), ipAdapter: deepClone(defaultIPAdapter) }, + ], }; dispatch(rgAdded({ isSelected: true, overrides })); }, [defaultIPAdapter, dispatch]); @@ -129,7 +142,7 @@ export const useAddGlobalReferenceImage = () => { const dispatch = useAppDispatch(); const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter); const func = useCallback(() => { - const overrides = { ipAdapter: defaultIPAdapter }; + const overrides = { ipAdapter: deepClone(defaultIPAdapter) }; dispatch(referenceImageAdded({ isSelected: true, overrides })); }, [defaultIPAdapter, dispatch]); @@ -140,7 +153,7 @@ export const useAddRegionalGuidanceIPAdapter = (entityIdentifier: CanvasEntityId const dispatch = useAppDispatch(); const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter); const func = useCallback(() => { - dispatch(rgIPAdapterAdded({ entityIdentifier, overrides: { ipAdapter: defaultIPAdapter } })); + dispatch(rgIPAdapterAdded({ entityIdentifier, overrides: { ipAdapter: deepClone(defaultIPAdapter) } })); }, [defaultIPAdapter, dispatch, entityIdentifier]); return func; diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index ad3a779aa87..a6f09377c5a 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -169,13 +169,13 @@ export const createNewCanvasEntityFromImage = (arg: { break; } case 'reference_image': { - const ipAdapter = selectDefaultIPAdapter(getState()); + const ipAdapter = deepClone(selectDefaultIPAdapter(getState())); ipAdapter.image = imageDTOToImageWithDims(imageDTO); dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true })); break; } case 'regional_guidance_with_reference_image': { - const ipAdapter = selectDefaultIPAdapter(getState()); + const ipAdapter = deepClone(selectDefaultIPAdapter(getState())); ipAdapter.image = imageDTOToImageWithDims(imageDTO); const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }]; dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true })); @@ -291,14 +291,14 @@ export const newCanvasFromImage = (arg: { break; } case 'reference_image': { - const ipAdapter = selectDefaultIPAdapter(getState()); + const ipAdapter = deepClone(selectDefaultIPAdapter(getState())); ipAdapter.image = imageDTOToImageWithDims(imageDTO); dispatch(canvasReset()); dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true })); break; } case 'regional_guidance_with_reference_image': { - const ipAdapter = selectDefaultIPAdapter(getState()); + const ipAdapter = deepClone(selectDefaultIPAdapter(getState())); ipAdapter.image = imageDTOToImageWithDims(imageDTO); const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }]; dispatch(canvasReset());