From 3001718f9f2f939ec2a50f74323a14cec329f4d3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:56:57 +1000 Subject: [PATCH] feat(ui): generalize mask fill, add to action bar --- invokeai/frontend/web/public/locales/en.json | 2 +- .../CanvasEntityList/EntityListActionBar.tsx | 4 +- .../EntityListActionBarSelectedEntityFill.tsx | 70 ++++++++++++++++++ .../components/InpaintMask/InpaintMask.tsx | 3 - .../InpaintMaskMaskFillColorPicker.tsx | 62 ---------------- .../RegionalGuidance/RegionalGuidance.tsx | 3 - .../RegionalGuidanceMaskFillColorPicker.tsx | 61 ---------------- .../controlLayers/store/canvasSlice.ts | 73 ++++++------------- .../features/controlLayers/store/selectors.ts | 17 +++++ .../src/features/controlLayers/store/types.ts | 6 ++ 10 files changed, 121 insertions(+), 180 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ff1af15a13..f1ec3a0fb7 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1676,7 +1676,7 @@ "resetRegion": "Reset Region", "debugLayers": "Debug Layers", "rectangle": "Rectangle", - "maskPreviewColor": "Mask Preview Color", + "maskFill": "Mask Fill", "addPositivePrompt": "Add $t(common.positivePrompt)", "addNegativePrompt": "Add $t(common.negativePrompt)", "addIPAdapter": "Add $t(common.ipAdapter)", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx index a0ddfc0fc9..758eb36b4e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx @@ -1,14 +1,16 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton'; import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton'; +import { EntityListActionBarSelectedEntityFill } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill'; import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity'; import { memo } from 'react'; export const EntityListActionBar = memo(() => { return ( - + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx new file mode 100644 index 0000000000..83c53f1f86 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx @@ -0,0 +1,70 @@ +import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import RgbColorPicker from 'common/components/RgbColorPicker'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; +import { entityFillColorChanged, entityFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectSelectedEntityFill, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const EntityListActionBarSelectedEntityFill = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const fill = useAppSelector(selectSelectedEntityFill); + + const onChangeFillColor = useCallback( + (color: RgbColor) => { + if (!selectedEntityIdentifier) { + return; + } + if (!isMaskEntityIdentifier(selectedEntityIdentifier)) { + return; + } + dispatch(entityFillColorChanged({ entityIdentifier: selectedEntityIdentifier, color })); + }, + [dispatch, selectedEntityIdentifier] + ); + const onChangeFillStyle = useCallback( + (style: FillStyle) => { + if (!selectedEntityIdentifier) { + return; + } + if (!isMaskEntityIdentifier(selectedEntityIdentifier)) { + return; + } + dispatch(entityFillStyleChanged({ entityIdentifier: selectedEntityIdentifier, style })); + }, + [dispatch, selectedEntityIdentifier] + ); + + if (!selectedEntityIdentifier || !fill) { + return null; + } + + return ( + + + + + + + + + + + + + + + + + + + + ); +}); + +EntityListActionBarSelectedEntityFill.displayName = 'EntityListActionBarSelectedEntityFill'; 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 e50221e7ef..9dcd945505 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -10,8 +10,6 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { InpaintMaskMaskFillColorPicker } from './InpaintMaskMaskFillColorPicker'; - type Props = { id: string; }; @@ -28,7 +26,6 @@ export const InpaintMask = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx deleted file mode 100644 index 6d2879eac6..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import RgbColorPicker from 'common/components/RgbColorPicker'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; -import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -export const InpaintMaskMaskFillColorPicker = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); - const selectFill = useMemo( - () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).fill), - [entityIdentifier] - ); - const fill = useAppSelector(selectFill); - - const onChangeFillColor = useCallback( - (color: RgbColor) => { - dispatch(inpaintMaskFillColorChanged({ entityIdentifier, color })); - }, - [dispatch, entityIdentifier] - ); - const onChangeFillStyle = useCallback( - (style: FillStyle) => { - dispatch(inpaintMaskFillStyleChanged({ entityIdentifier, style })); - }, - [dispatch, entityIdentifier] - ); - return ( - - - - - - - - - - - - - - ); -}); - -InpaintMaskMaskFillColorPicker.displayName = 'InpaintMaskMaskFillColorPicker'; 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 b122504785..e788cfba5f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -12,8 +12,6 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { RegionalGuidanceMaskFillColorPicker } from './RegionalGuidanceMaskFillColorPicker'; - type Props = { id: string; }; @@ -30,7 +28,6 @@ export const RegionalGuidance = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx deleted file mode 100644 index b950cf9995..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import RgbColorPicker from 'common/components/RgbColorPicker'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; -import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -export const RegionalGuidanceMaskFillColorPicker = memo(() => { - const entityIdentifier = useEntityIdentifierContext('regional_guidance'); - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectFill = useMemo( - () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).fill), - [entityIdentifier] - ); - const fill = useAppSelector(selectFill); - const onChangeFillColor = useCallback( - (color: RgbColor) => { - dispatch(rgFillColorChanged({ entityIdentifier, color })); - }, - [dispatch, entityIdentifier] - ); - const onChangeFillStyle = useCallback( - (style: FillStyle) => { - dispatch(rgFillStyleChanged({ entityIdentifier, style })); - }, - [dispatch, entityIdentifier] - ); - return ( - - - - - - - - - - - - - - ); -}); - -RegionalGuidanceMaskFillColorPicker.displayName = 'RegionalGuidanceMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 33ea62395d..c3c4958660 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -475,29 +475,6 @@ export const canvasSlice = createSlice({ } entity.negativePrompt = prompt; }, - rgFillColorChanged: ( - state, - action: PayloadAction> - ) => { - const { entityIdentifier, color } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.color = color; - }, - rgFillStyleChanged: ( - state, - action: PayloadAction> - ) => { - const { entityIdentifier, style } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.style = style; - }, - rgAutoNegativeToggled: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; const rg = selectEntity(state, entityIdentifier); @@ -658,28 +635,6 @@ export const canvasSlice = createSlice({ state.inpaintMasks.entities = [data]; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - inpaintMaskFillColorChanged: ( - state, - action: PayloadAction> - ) => { - const { color, entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.color = color; - }, - inpaintMaskFillStyleChanged: ( - state, - action: PayloadAction> - ) => { - const { style, entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.style = style; - }, //#region BBox bboxScaledSizeChanged: (state, action: PayloadAction>) => { state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; @@ -862,6 +817,28 @@ export const canvasSlice = createSlice({ } entity.isLocked = !entity.isLocked; }, + entityFillColorChanged: ( + state, + action: PayloadAction> + ) => { + const { color, entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.fill.color = color; + }, + entityFillStyleChanged: ( + state, + action: PayloadAction> + ) => { + const { style, entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.fill.style = style; + }, entityMoved: (state, action: PayloadAction) => { const { entityIdentifier, position } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -1088,6 +1065,8 @@ export const { entityReset, entityIsEnabledToggled, entityIsLockedToggled, + entityFillColorChanged, + entityFillStyleChanged, entityMoved, entityDuplicated, entityRasterized, @@ -1139,8 +1118,6 @@ export const { // rgRecalled, rgPositivePromptChanged, rgNegativePromptChanged, - rgFillColorChanged, - rgFillStyleChanged, rgAutoNegativeToggled, rgIPAdapterAdded, rgIPAdapterDeleted, @@ -1153,8 +1130,6 @@ export const { // Inpaint mask inpaintMaskAdded, // inpaintMaskRecalled, - inpaintMaskFillColorChanged, - inpaintMaskFillStyleChanged, } = canvasSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 92acd8ac1e..5ee05fdfca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -193,3 +193,20 @@ export const selectIsSelectedEntityDrawable = createSelector( export const selectCanvasMayUndo = (state: RootState) => state.canvas.past.length > 0; export const selectCanvasMayRedo = (state: RootState) => state.canvas.future.length > 0; +export const selectSelectedEntityFill = createSelector( + selectCanvasSlice, + selectSelectedEntityIdentifier, + (canvas, selectedEntityIdentifier) => { + if (!selectedEntityIdentifier) { + return null; + } + const entity = selectEntity(canvas, selectedEntityIdentifier); + if (!entity) { + return null; + } + if (entity.type !== 'inpaint_mask' && entity.type !== 'regional_guidance') { + return null; + } + return entity.fill; + } +); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 1986ddaca1..30a3edaef6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -788,3 +788,9 @@ export const getEntityIdentifier = ( ): CanvasEntityIdentifier => { return { id: entity.id, type: entity.type }; }; + +export const isMaskEntityIdentifier = ( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is CanvasEntityIdentifier<'inpaint_mask' | 'regional_guidance'> => { + return entityIdentifier.type === 'inpaint_mask' || entityIdentifier.type === 'regional_guidance'; +};