feat(ui): add merge visible for raster and inpaint mask layers

I don't think it makes sense to merge control layers or regional guidance layers because they have additional state.
This commit is contained in:
psychedelicious 2024-08-30 22:12:49 +10:00
parent 41f2ee2633
commit 0e354f5164
5 changed files with 152 additions and 19 deletions

View File

@ -1658,6 +1658,9 @@
"saveBboxToGallery": "Save Bbox To Gallery",
"savedToGalleryOk": "Saved to Gallery",
"savedToGalleryError": "Error saving to gallery",
"mergeVisible": "Merge Visible",
"mergeVisibleOk": "Merged visible layers",
"mergeVisibleError": "Error merging visible layers",
"clearHistory": "Clear History",
"generateMode": "Generate",
"generateModeDesc": "Create individual images. Generated images are added directly to the gallery.",

View File

@ -2,11 +2,12 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
import { useBoolean } from 'common/hooks/useBoolean';
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { memo, useMemo } from 'react';
import { PiCaretDownBold } from 'react-icons/pi';
type Props = PropsWithChildren<{
@ -21,6 +22,9 @@ const _hover: SystemStyleObject = {
export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => {
const title = useEntityTypeTitle(type);
const collapse = useBoolean(true);
const canMergeVisible = useMemo(() => type === 'raster_layer' || type === 'inpaint_mask', [type]);
const canHideAll = useMemo(() => type !== 'ip_adapter', [type]);
return (
<Flex flexDir="column" w="full">
<Flex w="full">
@ -54,8 +58,9 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
</Text>
<Spacer />
</Flex>
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
{type !== 'ip_adapter' && <CanvasEntityTypeIsHiddenToggle type={type} />}
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
</Flex>
<Collapse in={collapse.isTrue}>
<Flex flexDir="column" gap={2} pt={2}>

View File

@ -0,0 +1,88 @@
import { IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppDispatch } from 'app/store/storeHooks';
import { isOk, withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackBold } from 'react-icons/pi';
import { serializeError } from 'serialize-error';
const log = logger('canvas');
type Props = {
type: CanvasEntityIdentifier['type'];
};
export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const onClick = useCallback(async () => {
if (type === 'raster_layer') {
const rect = canvasManager.stage.getVisibleRect('raster_layer');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, false)
);
if (isOk(result)) {
dispatch(
rasterLayerAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
deleteOthers: true,
})
);
toast({ title: t('controlLayers.mergeVisibleOk') });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
}
} else if (type === 'inpaint_mask') {
const rect = canvasManager.stage.getVisibleRect('inpaint_mask');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeInpaintMask(rect, false)
);
if (isOk(result)) {
dispatch(
inpaintMaskAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
deleteOthers: true,
})
);
toast({ title: t('controlLayers.mergeVisibleOk') });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
}
} else {
log.error({ type }, 'Unsupported type for merge visible');
}
}, [canvasManager.compositor, canvasManager.stage, dispatch, t, type]);
return (
<IconButton
size="sm"
aria-label={t('controlLayers.mergeVisible')}
tooltip={t('controlLayers.mergeVisible')}
variant="link"
icon={<PiStackBold />}
onClick={onClick}
alignSelf="stretch"
/>
);
});
CanvasEntityMergeVisibleButton.displayName = 'CanvasEntityMergeVisibleButton';

View File

@ -179,6 +179,18 @@ export class CanvasCompositorModule extends CanvasModuleABC {
return imageDTO;
};
rasterizeAndUploadCompositeInpaintMask = async (rect: Rect, saveToGallery: boolean) => {
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
const canvas = this.getCompositeInpaintMaskCanvas(rect);
const blob = await canvasToBlob(canvas);
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite inpaint mask canvas');
}
return uploadImage(blob, 'composite-inpaint-mask.png', 'general', !saveToGallery);
};
getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
@ -193,15 +205,7 @@ export class CanvasCompositorModule extends CanvasModuleABC {
}
}
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
const canvas = this.getCompositeInpaintMaskCanvas(rect);
const blob = await canvasToBlob(canvas);
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite inpaint mask canvas');
}
imageDTO = await uploadImage(blob, 'composite-inpaint-mask.png', 'general', true);
imageDTO = await this.rasterizeAndUploadCompositeInpaintMask(rect, false);
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};

View File

@ -123,9 +123,14 @@ export const canvasSlice = createSlice({
rasterLayerAdded: {
reducer: (
state,
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }>
action: PayloadAction<{
id: string;
overrides?: Partial<CanvasRasterLayerState>;
isSelected?: boolean;
deleteOthers?: boolean;
}>
) => {
const { id, overrides, isSelected } = action.payload;
const { id, overrides, isSelected, deleteOthers } = action.payload;
const entity: CanvasRasterLayerState = {
id,
name: null,
@ -137,12 +142,25 @@ export const canvasSlice = createSlice({
position: { x: 0, y: 0 },
};
merge(entity, overrides);
state.rasterLayers.entities.push(entity);
if (deleteOthers) {
state.rasterLayers.entities = [entity];
} else {
state.rasterLayers.entities.push(entity);
}
if (isSelected) {
state.selectedEntityIdentifier = getEntityIdentifier(entity);
}
},
prepare: (payload: { overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }) => ({
prepare: (payload: {
overrides?: Partial<CanvasRasterLayerState>;
isSelected?: boolean;
/**
* asdf
*/
deleteOthers?: boolean;
}) => ({
payload: { ...payload, id: getPrefixedId('raster_layer') },
}),
},
@ -603,9 +621,14 @@ export const canvasSlice = createSlice({
inpaintMaskAdded: {
reducer: (
state,
action: PayloadAction<{ id: string; overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }>
action: PayloadAction<{
id: string;
overrides?: Partial<CanvasInpaintMaskState>;
isSelected?: boolean;
deleteOthers?: boolean;
}>
) => {
const { id, overrides, isSelected } = action.payload;
const { id, overrides, isSelected, deleteOthers } = action.payload;
const entity: CanvasInpaintMaskState = {
id,
name: null,
@ -621,12 +644,22 @@ export const canvasSlice = createSlice({
},
};
merge(entity, overrides);
state.inpaintMasks.entities.push(entity);
if (deleteOthers) {
state.inpaintMasks.entities = [entity];
} else {
state.inpaintMasks.entities.push(entity);
}
if (isSelected) {
state.selectedEntityIdentifier = getEntityIdentifier(entity);
}
},
prepare: (payload?: { overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }) => ({
prepare: (payload?: {
overrides?: Partial<CanvasInpaintMaskState>;
isSelected?: boolean;
deleteOthers?: boolean;
}) => ({
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
}),
},