feat(ui): generalize mask fill, add to action bar

This commit is contained in:
psychedelicious 2024-08-27 19:56:57 +10:00
parent e470eaf8f3
commit 0bf0bca03f
10 changed files with 121 additions and 180 deletions

View File

@ -1680,7 +1680,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)",

View File

@ -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 (
<Flex w="full" py={1} px={1} gap={2}>
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
<SelectedEntityOpacity />
<Spacer />
<EntityListActionBarSelectedEntityFill />
<EntityListActionBarAddLayerButton />
<EntityListActionBarDeleteButton />
</Flex>

View File

@ -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 (
<Popover isLazy>
<PopoverTrigger>
<Flex role="button" aria-label={t('controlLayers.maskFill')} tabIndex={-1} w={8} h={8}>
<Tooltip label={t('controlLayers.maskFill')}>
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<Box borderRadius="full" w={6} h={6} borderWidth={1} bg={rgbColorToString(fill.color)} />
</Flex>
</Tooltip>
</Flex>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<Flex flexDir="column" gap={4}>
<RgbColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<MaskFillStyle style={fill.style} onChange={onChangeFillStyle} />
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
EntityListActionBarSelectedEntityFill.displayName = 'EntityListActionBarSelectedEntityFill';

View File

@ -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) => {
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityIsLockedToggle />
<InpaintMaskMaskFillColorPicker />
<CanvasEntityEnabledToggle />
</CanvasEntityHeader>
</CanvasEntityContainer>

View File

@ -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 (
<Popover isLazy>
<PopoverTrigger>
<Flex
role="button"
aria-label={t('controlLayers.maskPreviewColor')}
borderRadius="full"
borderWidth={1}
bg={rgbColorToString(fill.color)}
w="22px"
h="22px"
tabIndex={-1}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<Flex flexDir="column" gap={4}>
<RgbColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<MaskFillStyle style={fill.style} onChange={onChangeFillStyle} />
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
InpaintMaskMaskFillColorPicker.displayName = 'InpaintMaskMaskFillColorPicker';

View File

@ -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) => {
<CanvasEntityEditableTitle />
<Spacer />
<RegionalGuidanceBadges />
<RegionalGuidanceMaskFillColorPicker />
<CanvasEntityIsLockedToggle />
<CanvasEntityEnabledToggle />
</CanvasEntityHeader>

View File

@ -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 (
<Popover isLazy>
<PopoverTrigger>
<Flex
role="button"
aria-label={t('controlLayers.maskPreviewColor')}
borderRadius="full"
borderWidth={1}
bg={rgbColorToString(fill.color)}
w="22px"
h="22px"
tabIndex={-1}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<Flex flexDir="column" gap={4}>
<RgbColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<MaskFillStyle style={fill.style} onChange={onChangeFillStyle} />
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
RegionalGuidanceMaskFillColorPicker.displayName = 'RegionalGuidanceMaskFillColorPicker';

View File

@ -475,29 +475,6 @@ export const canvasSlice = createSlice({
}
entity.negativePrompt = prompt;
},
rgFillColorChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'regional_guidance'>>
) => {
const { entityIdentifier, color } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.color = color;
},
rgFillStyleChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'regional_guidance'>>
) => {
const { entityIdentifier, style } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.style = style;
},
rgAutoNegativeToggled: (state, action: PayloadAction<EntityIdentifierPayload<void, 'regional_guidance'>>) => {
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<EntityIdentifierPayload<{ color: RgbColor }, 'inpaint_mask'>>
) => {
const { color, entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.color = color;
},
inpaintMaskFillStyleChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'inpaint_mask'>>
) => {
const { style, entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.style = style;
},
//#region BBox
bboxScaledSizeChanged: (state, action: PayloadAction<Partial<Dimensions>>) => {
state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload };
@ -862,6 +817,28 @@ export const canvasSlice = createSlice({
}
entity.isLocked = !entity.isLocked;
},
entityFillColorChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'inpaint_mask' | 'regional_guidance'>>
) => {
const { color, entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.color = color;
},
entityFillStyleChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'inpaint_mask' | 'regional_guidance'>>
) => {
const { style, entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.style = style;
},
entityMoved: (state, action: PayloadAction<EntityMovedPayload>) => {
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 */

View File

@ -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;
}
);

View File

@ -788,3 +788,9 @@ export const getEntityIdentifier = <T extends CanvasEntityType>(
): CanvasEntityIdentifier<T> => {
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';
};