Skip to content

feat(ui): canvas entity merging #7219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1647,8 +1647,9 @@
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
"regionIsEmpty": "Selected region is empty",
"mergeVisible": "Merge Visible",
"mergeVisibleOk": "Merged visible layers",
"mergeVisibleError": "Error merging visible layers",
"mergeDown": "Merge Down",
"mergeVisibleOk": "Merged layers",
"mergeVisibleError": "Error merging layers",
"clearHistory": "Clear History",
"bboxOverlay": "Show Bbox Overlay",
"resetCanvas": "Reset Canvas",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/componen
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
Expand All @@ -27,6 +28,7 @@ export const ControlLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsSelectObject />
<ControlLayerMenuItemsTransparencyEffect />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<ControlLayerMenuItemsCopyToSubMenu />
<ControlLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { InpaintMaskMenuItemsConvertToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsConvertToSubMenu';
import { InpaintMaskMenuItemsCopyToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsCopyToSubMenu';
Expand All @@ -20,9 +22,11 @@ export const InpaintMaskMenuItems = memo(() => {
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<InpaintMaskMenuItemsCopyToSubMenu />
<InpaintMaskMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave />
</>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/componen
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
Expand All @@ -25,6 +26,7 @@ export const RasterLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsFilter />
<CanvasEntityMenuItemsSelectObject />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<RasterLayerMenuItemsCopyToSubMenu />
<RasterLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter';
import { RegionalGuidanceMenuItemsAutoNegative } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative';
Expand All @@ -25,9 +27,11 @@ export const RegionalGuidanceMenuItems = memo(() => {
<CanvasEntityMenuItemsTransform />
<RegionalGuidanceMenuItemsAutoNegative />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<RegionalGuidanceMenuItemsCopyToSubMenu />
<RegionalGuidanceMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave />
</>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/component
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/useEntityTypeInformationalPopover';
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { type CanvasEntityIdentifier, isRenderableEntityType } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { PiCaretDownBold } from 'react-icons/pi';

type Props = PropsWithChildren<{
Expand All @@ -25,8 +25,6 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
const title = useEntityTypeTitle(type);
const informationalPopoverFeature = useEntityTypeInformationalPopover(type);
const collapse = useBoolean(true);
const canMergeVisible = useMemo(() => type === 'raster_layer' || type === 'inpaint_mask', [type]);
const canHideAll = useMemo(() => type !== 'reference_image', [type]);

return (
<Flex flexDir="column" w="full">
Expand Down Expand Up @@ -76,8 +74,8 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props

<Spacer />
</Flex>
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
{isRenderableEntityType(type) && <CanvasEntityMergeVisibleButton type={type} />}
{isRenderableEntityType(type) && <CanvasEntityTypeIsHiddenToggle type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
</Flex>
<Collapse in={collapse.isTrue}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier';
import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackSimpleBold } from 'react-icons/pi';

export const CanvasEntityMenuItemsMergeDown = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
const entityIdentifier = useEntityIdentifierContext<CanvasRenderableEntityType>();
const entityIdentifierBelowThisOne = useEntityIdentifierBelowThisOne(entityIdentifier);
const mergeDown = useCallback(() => {
if (entityIdentifierBelowThisOne === null) {
return;
}
canvasManager.compositor.mergeByEntityIdentifiers([entityIdentifierBelowThisOne, entityIdentifier], true);
}, [canvasManager.compositor, entityIdentifier, entityIdentifierBelowThisOne]);

return (
<MenuItem
onClick={mergeDown}
icon={<PiStackSimpleBold />}
isDisabled={isBusy || entityIdentifierBelowThisOne === null}
>
{t('controlLayers.mergeDown')}
</MenuItem>
);
});

CanvasEntityMenuItemsMergeDown.displayName = 'CanvasEntityMenuItemsMergeDown';
Original file line number Diff line number Diff line change
@@ -1,80 +1,24 @@
import { IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppDispatch } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityTypeCount } from 'features/controlLayers/hooks/useEntityTypeCount';
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { toast } from 'features/toast/toast';
import { useVisibleEntityCountByType } from 'features/controlLayers/hooks/useVisibleEntityCountByType';
import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
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'];
type: CanvasRenderableEntityType;
};

export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
const entityCount = useEntityTypeCount(type);
const onClick = useCallback(async () => {
if (type === 'raster_layer') {
const rect = canvasManager.stage.getVisibleRect('raster_layer');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, { is_intermediate: true })
);

if (result.isOk()) {
dispatch(
rasterLayerAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
isMergingVisible: 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 (result.isOk()) {
dispatch(
inpaintMaskAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
isMergingVisible: 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]);
const entityCount = useVisibleEntityCountByType(type);
const mergeVisible = useCallback(() => {
canvasManager.compositor.mergeVisibleOfType(type);
}, [canvasManager.compositor, type]);

return (
<IconButton
Expand All @@ -83,7 +27,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
tooltip={t('controlLayers.mergeVisible')}
variant="link"
icon={<PiStackBold />}
onClick={onClick}
onClick={mergeVisible}
alignSelf="stretch"
isDisabled={entityCount <= 1 || isBusy}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit

const saveCanvas = useCallback(async () => {
const rect =
region === 'bbox' ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect('raster_layer');
region === 'bbox'
? canvasManager.stateApi.getBbox().rect
: canvasManager.compositor.getVisibleRectOfType('raster_layer');

if (rect.width === 0 || rect.height === 0) {
toast({
Expand All @@ -68,12 +70,13 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
metadata = selectCanvasMetadata(store.getState());
}

const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, {
const result = await withResultAsync(() => {
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
return canvasManager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
is_intermediate: !saveToGallery,
metadata,
})
);
});
});

if (result.isOk()) {
if (onSave) {
Expand All @@ -86,7 +89,6 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
}
}, [
canvasManager.compositor,
canvasManager.stage,
canvasManager.stateApi,
onSave,
region,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice, selectEntityIdentifierBelowThisOne } from 'features/controlLayers/store/selectors';
import type { CanvasRenderableEntityIdentifier } from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';

export const useEntityIdentifierBelowThisOne = <T extends CanvasRenderableEntityIdentifier>(
entityIdentifier: T
): T | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const nextEntity = selectEntityIdentifierBelowThisOne(canvas, entityIdentifier);
if (!nextEntity) {
return null;
}
return getEntityIdentifier(nextEntity);
}),
[entityIdentifier]
);
const entityIdentifierBelowThisOne = useAppSelector(selector);

return entityIdentifierBelowThisOne as T | null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import {
selectActiveControlLayerEntities,
selectActiveInpaintMaskEntities,
selectActiveRasterLayerEntities,
selectActiveReferenceImageEntities,
selectActiveRegionalGuidanceEntities,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { assert } from 'tsafe';

export const useVisibleEntityCountByType = (type: CanvasEntityIdentifier['type']): number => {
const selectVisibleEntityCountByType = useMemo(() => {
switch (type) {
case 'control_layer':
return createSelector(selectActiveControlLayerEntities, (entities) => entities.length);
case 'raster_layer':
return createSelector(selectActiveRasterLayerEntities, (entities) => entities.length);
case 'inpaint_mask':
return createSelector(selectActiveInpaintMaskEntities, (entities) => entities.length);
case 'regional_guidance':
return createSelector(selectActiveRegionalGuidanceEntities, (entities) => entities.length);
case 'reference_image':
return createSelector(selectActiveReferenceImageEntities, (entities) => entities.length);
default:
assert(false, 'Invalid entity type');
}
}, [type]);
const visibleEntityCount = useAppSelector(selectVisibleEntityCountByType);
return visibleEntityCount;
};
Loading
Loading