mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
41f2ee2633
commit
0e354f5164
@ -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.",
|
||||
|
@ -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}>
|
||||
|
@ -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';
|
@ -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;
|
||||
};
|
||||
|
@ -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') },
|
||||
}),
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user