mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): generalize mask fill, add to action bar
This commit is contained in:
parent
e470eaf8f3
commit
0bf0bca03f
@ -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)",
|
||||
|
@ -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>
|
||||
|
@ -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';
|
@ -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>
|
||||
|
@ -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';
|
@ -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>
|
||||
|
@ -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';
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
@ -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';
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user