diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 9fd475d476..d923980c6d 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1677,6 +1677,7 @@ "controlLayers_withCount": "Control Layers ({{count}})", "rasterLayers_withCount": "Raster Layers ({{count}})", "ipAdapters_withCount": "IP Adapters ({{count}})", + "inpaintMasks_withCount": "Inpaint Masks ({{count}})", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 335a6d126d..5eb05fe95b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,13 @@ import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { controlLayerAdded, ipaAdded, rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { + controlLayerAdded, + inpaintMaskAdded, + ipaAdded, + rasterLayerAdded, + rgAdded, +} from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -11,7 +17,10 @@ export const AddLayerButton = memo(() => { const dispatch = useAppDispatch(); const defaultControlAdapter = useDefaultControlAdapter(); const defaultIPAdapter = useDefaultIPAdapter(); - const addRGLayer = useCallback(() => { + const addInpaintMask = useCallback(() => { + dispatch(inpaintMaskAdded()); + }, [dispatch]); + const addRegionalGuidance = useCallback(() => { dispatch(rgAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { @@ -34,7 +43,8 @@ export const AddLayerButton = memo(() => { data-testid="control-layers-add-layer-menu-button" /> - {t('controlLayers.regionalGuidanceLayer')} + {t('controlLayers.inpaintMask')} + {t('controlLayers.regionalGuidance')} {t('controlLayers.rasterLayer')} {t('controlLayers.controlLayer')} {t('controlLayers.globalIPAdapterLayer')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index ac5878b283..2b28bdc61e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { CanvasEntityOpacity } from 'features/controlLayers/components/common/CanvasEntityOpacity'; import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList'; -import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; +import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList'; import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList'; import { RasterLayerEntityList } from 'features/controlLayers/components/RasterLayer/RasterLayerEntityList'; import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList'; @@ -13,7 +13,7 @@ export const CanvasEntityList = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index a6dca5eb90..4bc1d3409d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -54,7 +54,7 @@ const ControlLayersSettingsPopover = () => { for (const adapter of canvasManager.regionalGuidanceAdapters.values()) { adapter.transformer.requestRectCalculation(); } - canvasManager.inpaintMaskAdapter.transformer.requestRectCalculation(); + canvasManager.inpaintMaskAdapters.transformer.requestRectCalculation(); }, [canvasManager]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index 186b2fad3f..02a5a64807 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -1,36 +1,32 @@ -import { Flex, Spacer } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; import { InpaintMaskMaskFillColorPicker } from './InpaintMaskMaskFillColorPicker'; -export const InpaintMask = memo(() => { - const { t } = useTranslation(); - const entityIdentifier = useMemo(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []); - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask')); +type Props = { + id: string; +}; + +export const InpaintMask = memo(({ id }: Props) => { + const entityIdentifier = useMemo(() => ({ id, type: 'inpaint_mask' }), [id]); return ( - - - - - - - - - - - - - + + + + + + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx new file mode 100644 index 0000000000..b5d72bc675 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx @@ -0,0 +1,38 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; +import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + return canvasV2.inpaintMasks.entities.map(mapId).reverse(); +}); + +export const InpaintMaskList = memo(() => { + const { t } = useTranslation(); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask')); + const entityIds = useAppSelector(selectEntityIds); + + if (entityIds.length === 0) { + return null; + } + + if (entityIds.length > 0) { + return ( + + {entityIds.map((id) => ( + + ))} + + ); + } +}); + +InpaintMaskList.displayName = 'InpaintMaskList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx index 42951878b4..ef2cf79a4a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx @@ -1,29 +1,33 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; +import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; -import { imFillColorChanged, imFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectInpaintMaskEntityOrThrow } from 'features/controlLayers/store/inpaintMaskReducers'; +import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const InpaintMaskMaskFillColorPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill); + const entityIdentifier = useEntityIdentifierContext(); + const fill = useAppSelector((s) => selectInpaintMaskEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); + const onChangeFillColor = useCallback( - (color: RgbaColor) => { - dispatch(imFillColorChanged({ color })); + (color: RgbColor) => { + dispatch(inpaintMaskFillColorChanged({ entityIdentifier, color })); }, - [dispatch] + [dispatch, entityIdentifier] ); const onChangeFillStyle = useCallback( (style: FillStyle) => { - dispatch(imFillStyleChanged({ style })); + dispatch(inpaintMaskFillStyleChanged({ entityIdentifier, style })); }, - [dispatch] + [dispatch, entityIdentifier] ); return ( @@ -43,7 +47,7 @@ export const InpaintMaskMaskFillColorPicker = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index 4f1780d6f6..a6bfa79958 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -1,13 +1,13 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; +import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; -import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types'; +import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,7 +17,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { const dispatch = useAppDispatch(); const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); const onChangeFillColor = useCallback( - (color: RgbaColor) => { + (color: RgbColor) => { dispatch(rgFillColorChanged({ id: entityIdentifier.id, color })); }, [dispatch, entityIdentifier.id] @@ -46,7 +46,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 11feea75b4..49da41d50e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -48,7 +48,7 @@ export class CanvasManager { rasterLayerAdapters: Map = new Map(); controlLayerAdapters: Map = new Map(); regionalGuidanceAdapters: Map = new Map(); - inpaintMaskAdapter: CanvasMaskAdapter; + inpaintMaskAdapters: Map = new Map(); stateApi: CanvasStateApi; preview: CanvasPreview; background: CanvasBackground; @@ -120,9 +120,6 @@ export class CanvasManager { this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier); this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); - - this.inpaintMaskAdapter = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); - this.stage.add(this.inpaintMaskAdapter.konva.layer); } enableDebugging() { @@ -149,19 +146,21 @@ export class CanvasManager { this.background.konva.layer.zIndex(++zIndex); - for (const layer of this.stateApi.getRasterLayersState().entities) { - this.rasterLayerAdapters.get(layer.id)?.konva.layer.zIndex(++zIndex); + for (const { id } of this.stateApi.getRasterLayersState().entities) { + this.rasterLayerAdapters.get(id)?.konva.layer.zIndex(++zIndex); } - for (const layer of this.stateApi.getControlLayersState().entities) { - this.controlLayerAdapters.get(layer.id)?.konva.layer.zIndex(++zIndex); + for (const { id } of this.stateApi.getControlLayersState().entities) { + this.controlLayerAdapters.get(id)?.konva.layer.zIndex(++zIndex); } - for (const rg of this.stateApi.getRegionsState().entities) { - this.regionalGuidanceAdapters.get(rg.id)?.konva.layer.zIndex(++zIndex); + for (const { id } of this.stateApi.getRegionsState().entities) { + this.regionalGuidanceAdapters.get(id)?.konva.layer.zIndex(++zIndex); } - this.inpaintMaskAdapter.konva.layer.zIndex(++zIndex); + for (const { id } of this.stateApi.getInpaintMasksState().entities) { + this.inpaintMaskAdapters.get(id)?.konva.layer.zIndex(++zIndex); + } this.preview.getLayer().zIndex(++zIndex); } @@ -181,8 +180,10 @@ export class CanvasManager { getVisibleRect = (): Rect => { const rects = []; - if (this.inpaintMaskAdapter.state.isEnabled) { - rects.push(this.inpaintMaskAdapter.transformer.getRelativeRect()); + for (const adapter of this.inpaintMaskAdapters.values()) { + if (adapter.state.isEnabled) { + rects.push(adapter.transformer.getRelativeRect()); + } } for (const adapter of this.rasterLayerAdapters.values()) { @@ -241,7 +242,7 @@ export class CanvasManager { }); } - getTransformingLayer() { + getTransformingLayer = (): CanvasLayerAdapter | CanvasMaskAdapter | null => { const transformingEntity = this.stateApi.$transformingEntity.get(); if (!transformingEntity) { return null; @@ -254,13 +255,13 @@ export class CanvasManager { } else if (type === 'control_layer') { return this.controlLayerAdapters.get(id) ?? null; } else if (type === 'inpaint_mask') { - return this.inpaintMaskAdapter; + return this.inpaintMaskAdapters.get(id) ?? null; } else if (type === 'regional_guidance') { return this.regionalGuidanceAdapters.get(id) ?? null; } return null; - } + }; getIsTransforming() { return Boolean(this.stateApi.$transformingEntity.get()); @@ -397,16 +398,33 @@ export class CanvasManager { if ( isFirstRender || - state.inpaintMask !== prevState.inpaintMask || + state.inpaintMasks.entities !== prevState.inpaintMasks.entities || state.tool.selected !== prevState.tool.selected || state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id ) { - this.log.debug('Rendering inpaint mask'); - await this.inpaintMaskAdapter.update({ - state: state.inpaintMask, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === state.inpaintMask.id, - }); + this.log.debug('Rendering inpaint masks'); + + // Destroy the konva nodes for nonexistent entities + for (const adapter of this.inpaintMaskAdapters.values()) { + if (!state.inpaintMasks.entities.find((rg) => rg.id === adapter.id)) { + adapter.destroy(); + this.inpaintMaskAdapters.delete(adapter.id); + } + } + + for (const entityState of state.inpaintMasks.entities) { + let adapter = this.inpaintMaskAdapters.get(entityState.id); + if (!adapter) { + adapter = new CanvasMaskAdapter(entityState, this); + this.inpaintMaskAdapters.set(adapter.id, adapter); + this.stage.add(adapter.konva.layer); + } + await adapter.update({ + state: entityState, + toolState: state.tool, + isSelected: state.selectedEntityIdentifier?.id === entityState.id, + }); + } } this.stateApi.$toolState.set(state.tool); @@ -427,8 +445,9 @@ export class CanvasManager { if ( isFirstRender || state.rasterLayers.entities !== prevState.rasterLayers.entities || + state.controlLayers.entities !== prevState.controlLayers.entities || state.regions.entities !== prevState.regions.entities || - state.inpaintMask !== prevState.inpaintMask || + state.inpaintMasks.entities !== prevState.inpaintMasks.entities || state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id ) { this.log.debug('Arranging entities'); @@ -454,14 +473,13 @@ export class CanvasManager { return () => { this.log.debug('Cleaning up konva renderer'); - this.inpaintMaskAdapter.destroy(); - for (const adapter of this.regionalGuidanceAdapters.values()) { - adapter.destroy(); - } - for (const adapter of this.rasterLayerAdapters.values()) { - adapter.destroy(); - } - for (const adapter of this.controlLayerAdapters.values()) { + const allAdapters = [ + ...this.rasterLayerAdapters.values(), + ...this.controlLayerAdapters.values(), + ...this.inpaintMaskAdapters.values(), + ...this.regionalGuidanceAdapters.values(), + ]; + for (const adapter of allAdapters) { adapter.destroy(); } this.background.destroy(); @@ -558,7 +576,7 @@ export class CanvasManager { return pixels / this.getStageScale(); } - getCompositeLayerStageClone = (): Konva.Stage => { + getCompositeRasterLayerStageClone = (): Konva.Stage => { const layersState = this.stateApi.getRasterLayersState(); const stageClone = this.stage.clone(); @@ -580,58 +598,95 @@ export class CanvasManager { return stageClone; }; - getCompositeLayerBlob = (rect?: Rect): Promise => { - return konvaNodeToBlob(this.getCompositeLayerStageClone(), rect); + getCompositeInpaintMaskStageClone = (): Konva.Stage => { + const entities = this.stateApi.getInpaintMasksState().entities; + const validEntities = entities.filter((entity) => entity.isEnabled && entity.objects.length > 0); + + const stageClone = this.stage.clone(); + + stageClone.scaleX(1); + stageClone.scaleY(1); + stageClone.x(0); + stageClone.y(0); + + // getLayers() returns the internal `children` array of the stage directly - calling destroy on a layer will + // mutate that array. We need to clone the array to avoid mutating the original. + for (const konvaLayer of stageClone.getLayers().slice()) { + if (!validEntities.find((l) => l.id === konvaLayer.id())) { + konvaLayer.destroy(); + } + } + + return stageClone; }; - getCompositeLayerImageData = (rect?: Rect): ImageData => { - return konvaNodeToImageData(this.getCompositeLayerStageClone(), rect); - }; - - getCompositeRasterizedImageCache = (rect: Rect): ImageCache | null => { - const layerState = this.stateApi.getRasterLayersState(); - const imageCache = layerState.compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); + getCompositeInpaintMaskImageCache = (rect: Rect): ImageCache | null => { + const { compositeRasterizationCache } = this.stateApi.getInpaintMasksState(); + const imageCache = compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); return imageCache ?? null; }; - getCompositeLayerImageDTO = async (rect: Rect): Promise => { + getCompositeRasterLayerImageCache = (rect: Rect): ImageCache | null => { + const { compositeRasterizationCache } = this.stateApi.getRasterLayersState(); + const imageCache = compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); + return imageCache ?? null; + }; + + getCompositeRasterLayerImageDTO = async (rect: Rect): Promise => { let imageDTO: ImageDTO | null = null; - const compositeRasterizedImageCache = this.getCompositeRasterizedImageCache(rect); + const compositeRasterizedImageCache = this.getCompositeRasterLayerImageCache(rect); if (compositeRasterizedImageCache) { imageDTO = await getImageDTO(compositeRasterizedImageCache.imageName); if (imageDTO) { - this.log.trace({ rect, compositeRasterizedImageCache, imageDTO }, 'Using cached composite rasterized image'); + this.log.trace({ rect, compositeRasterizedImageCache, imageDTO }, 'Using cached composite raster layer image'); return imageDTO; } } - this.log.trace({ rect }, 'Rasterizing composite layer'); + this.log.trace({ rect }, 'Rasterizing composite raster layer'); - const blob = await this.getCompositeLayerBlob(rect); + const blob = await konvaNodeToBlob(this.getCompositeRasterLayerStageClone(), rect); if (this._isDebugging) { - previewBlob(blob, 'Rasterized entity'); + previewBlob(blob, 'Composite raster layer'); } - imageDTO = await uploadImage(blob, 'composite-layer.png', 'general', true); - this.stateApi.compositeLayerRasterized({ imageName: imageDTO.image_name, rect }); + imageDTO = await uploadImage(blob, 'composite-raster-layer.png', 'general', true); + this.stateApi.compositeRasterLayerRasterized({ imageName: imageDTO.image_name, rect }); return imageDTO; }; - getInpaintMaskBlob = (rect?: Rect): Promise => { - return this.inpaintMaskAdapter.renderer.getBlob(rect); - }; + getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise => { + let imageDTO: ImageDTO | null = null; + const compositeRasterizedImageCache = this.getCompositeInpaintMaskImageCache(rect); - getInpaintMaskImageData = (rect?: Rect): ImageData => { - return this.inpaintMaskAdapter.renderer.getImageData(rect); + if (compositeRasterizedImageCache) { + imageDTO = await getImageDTO(compositeRasterizedImageCache.imageName); + if (imageDTO) { + this.log.trace({ rect, compositeRasterizedImageCache, imageDTO }, 'Using cached composite inpaint mask image'); + return imageDTO; + } + } + + this.log.trace({ rect }, 'Rasterizing composite inpaint mask'); + + const blob = await konvaNodeToBlob(this.getCompositeInpaintMaskStageClone(), rect); + + if (this._isDebugging) { + previewBlob(blob, 'Composite inpaint mask'); + } + + imageDTO = await uploadImage(blob, 'composite-inpaint-mask.png', 'general', true); + this.stateApi.compositeInpaintMaskRasterized({ imageName: imageDTO.image_name, rect }); + return imageDTO; }; getGenerationMode(): GenerationMode { const { rect } = this.stateApi.getBbox(); - const inpaintMaskImageData = this.getInpaintMaskImageData(rect); + const inpaintMaskImageData = konvaNodeToImageData(this.getCompositeInpaintMaskStageClone(), rect); const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); - const compositeLayerImageData = this.getCompositeLayerImageData(rect); + const compositeLayerImageData = konvaNodeToImageData(this.getCompositeRasterLayerStageClone(), rect); const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); if (compositeLayerTransparency === 'FULLY_TRANSPARENT') { // When the initial image is fully transparent, we are always doing txt2img diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 32a7e5615e..2313e6864e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -27,6 +27,7 @@ import { entitySelected, eraserWidthChanged, fillChanged, + inpaintMaskCompositeRasterized, rasterLayerCompositeRasterized, toolBufferChanged, toolChanged, @@ -110,9 +111,12 @@ export class CanvasStateApi { rasterizeEntity = (arg: EntityRasterizedPayload) => { this._store.dispatch(entityRasterized(arg)); }; - compositeLayerRasterized = (arg: { imageName: string; rect: Rect }) => { + compositeRasterLayerRasterized = (arg: { imageName: string; rect: Rect }) => { this._store.dispatch(rasterLayerCompositeRasterized(arg)); }; + compositeInpaintMaskRasterized = (arg: { imageName: string; rect: Rect }) => { + this._store.dispatch(inpaintMaskCompositeRasterized(arg)); + }; setSelectedEntity = (arg: EntityIdentifierPayload) => { this._store.dispatch(entitySelected(arg)); }; @@ -154,8 +158,8 @@ export class CanvasStateApi { getControlLayersState = () => { return this.getState().controlLayers; }; - getInpaintMaskState = () => { - return this.getState().inpaintMask; + getInpaintMasksState = () => { + return this.getState().inpaintMasks; }; getSession = () => { return this.getState().session; @@ -183,8 +187,8 @@ export class CanvasStateApi { entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; entityAdapter = this.manager.regionalGuidanceAdapters.get(identifier.id) ?? null; } else if (identifier.type === 'inpaint_mask') { - entityState = state.inpaintMask; - entityAdapter = this.manager.inpaintMaskAdapter; + entityState = state.inpaintMasks.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.inpaintMaskAdapters.get(identifier.id) ?? null; } if (entityState && entityAdapter) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index dc6b5b53e1..2c49c7c21e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -41,32 +41,17 @@ import type { FilterConfig, StageAttrs, } from './types'; -import { IMAGE_FILTERS, isDrawableEntity } from './types'; +import { getEntityIdentifier, IMAGE_FILTERS, isDrawableEntity } from './types'; const initialState: CanvasV2State = { _version: 3, - selectedEntityIdentifier: { id: 'inpaint_mask', type: 'inpaint_mask' }, + selectedEntityIdentifier: null, rasterLayers: { entities: [], compositeRasterizationCache: [] }, controlLayers: { entities: [] }, ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], - inpaintMask: { - id: 'inpaint_mask', - type: 'inpaint_mask', - fill: { - style: 'diagonal', - color: { r: 255, g: 122, b: 0 }, // some orange color - }, - rasterizationCache: [], - isEnabled: true, - objects: [], - opacity: 1, - position: { - x: 0, - y: 0, - }, - }, + inpaintMasks: { entities: [], compositeRasterizationCache: [] }, tool: { selected: 'view', selectedBuffer: null, @@ -148,15 +133,15 @@ const initialState: CanvasV2State = { export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) { switch (type) { case 'raster_layer': - return state.rasterLayers.entities.find((layer) => layer.id === id); + return state.rasterLayers.entities.find((entity) => entity.id === id); case 'control_layer': - return state.controlLayers.entities.find((layer) => layer.id === id); + return state.controlLayers.entities.find((entity) => entity.id === id); case 'inpaint_mask': - return state.inpaintMask; + return state.inpaintMasks.entities.find((entity) => entity.id === id); case 'regional_guidance': - return state.regions.entities.find((rg) => rg.id === id); + return state.regions.entities.find((entity) => entity.id === id); case 'ip_adapter': - return state.ipAdapters.entities.find((ipa) => ipa.id === id); + return state.ipAdapters.entities.find((entity) => entity.id === id); default: return; } @@ -168,7 +153,7 @@ export function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntity } else if (type === 'control_layer') { return state.controlLayers.entities; } else if (type === 'inpaint_mask') { - return [state.inpaintMask]; + return state.inpaintMasks.entities; } else if (type === 'regional_guidance') { return state.regions.entities; } else if (type === 'ip_adapter') { @@ -194,6 +179,8 @@ const invalidateRasterizationCaches = ( // its cache. if (entity.type === 'raster_layer') { state.rasterLayers.compositeRasterizationCache = []; + } else if (entity.type === 'inpaint_mask') { + state.inpaintMasks.compositeRasterizationCache = []; } }; @@ -223,10 +210,6 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'inpaint_mask') { - // Inpaint mask cannot be renamed - return; - } entity.name = name; }, entityReset: (state, action: PayloadAction) => { @@ -331,7 +314,11 @@ export const canvasV2Slice = createSlice({ entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - let selectedEntityIdentifier: CanvasEntityIdentifier = { type: state.inpaintMask.type, id: state.inpaintMask.id }; + const firstInpaintMaskEntity = state.inpaintMasks.entities[0]; + + let selectedEntityIdentifier: CanvasV2State['selectedEntityIdentifier'] = firstInpaintMaskEntity + ? getEntityIdentifier(firstInpaintMaskEntity) + : null; if (entityIdentifier.type === 'raster_layer') { const index = state.rasterLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); @@ -363,6 +350,15 @@ export const canvasV2Slice = createSlice({ if (entity) { selectedEntityIdentifier = { type: entity.type, id: entity.id }; } + } else if (entityIdentifier.type === 'inpaint_mask') { + const index = state.inpaintMasks.entities.findIndex((layer) => layer.id === entityIdentifier.id); + state.inpaintMasks.entities = state.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id); + // When deleting a inpaint mask, we need to invalidate the composite rasterization cache. + state.inpaintMasks.compositeRasterizationCache = []; + const entity = state.inpaintMasks.entities[index]; + if (entity) { + selectedEntityIdentifier = { type: entity.type, id: entity.id }; + } } else { assert(false, 'Not implemented'); } @@ -383,6 +379,10 @@ export const canvasV2Slice = createSlice({ moveOneToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveOneToEnd(state.regions.entities, entity); + } else if (entity.type === 'inpaint_mask') { + moveOneToEnd(state.inpaintMasks.entities, entity); + // When arranging a inpaint mask, we need to invalidate the composite rasterization cache. + state.inpaintMasks.compositeRasterizationCache = []; } }, entityArrangedToFront: (state, action: PayloadAction) => { @@ -399,6 +399,10 @@ export const canvasV2Slice = createSlice({ moveToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveToEnd(state.regions.entities, entity); + } else if (entity.type === 'inpaint_mask') { + moveToEnd(state.inpaintMasks.entities, entity); + // When arranging a inpaint mask, we need to invalidate the composite rasterization cache. + state.inpaintMasks.compositeRasterizationCache = []; } }, entityArrangedBackwardOne: (state, action: PayloadAction) => { @@ -415,6 +419,10 @@ export const canvasV2Slice = createSlice({ moveOneToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveOneToStart(state.regions.entities, entity); + } else if (entity.type === 'inpaint_mask') { + moveOneToStart(state.inpaintMasks.entities, entity); + // When arranging a inpaint mask, we need to invalidate the composite rasterization cache. + state.inpaintMasks.compositeRasterizationCache = []; } }, entityArrangedToBack: (state, action: PayloadAction) => { @@ -424,13 +432,17 @@ export const canvasV2Slice = createSlice({ return; } if (entity.type === 'raster_layer') { - // When arranging a raster layer, we need to invalidate the composite rasterization cache. moveToStart(state.rasterLayers.entities, entity); + // When arranging a raster layer, we need to invalidate the composite rasterization cache. state.rasterLayers.compositeRasterizationCache = []; } else if (entity.type === 'control_layer') { moveToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveToStart(state.regions.entities, entity); + } else if (entity.type === 'inpaint_mask') { + moveToStart(state.inpaintMasks.entities, entity); + // When arranging a inpaint mask, we need to invalidate the composite rasterization cache. + state.inpaintMasks.compositeRasterizationCache = []; } }, entityOpacityChanged: (state, action: PayloadAction>) => { @@ -462,7 +474,7 @@ export const canvasV2Slice = createSlice({ entities = state.controlLayers.entities; break; case 'inpaint_mask': - entities = [state.inpaintMask]; + entities = state.inpaintMasks.entities; break; case 'regional_guidance': entities = state.regions.entities; @@ -479,28 +491,28 @@ export const canvasV2Slice = createSlice({ allEntitiesDeleted: (state) => { state.ipAdapters = deepClone(initialState.ipAdapters); state.rasterLayers = deepClone(initialState.rasterLayers); - state.rasterLayers.compositeRasterizationCache = []; state.controlLayers = deepClone(initialState.controlLayers); state.regions = deepClone(initialState.regions); - state.inpaintMask = deepClone(initialState.inpaintMask); + state.inpaintMasks = deepClone(initialState.inpaintMasks); + state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); }, rasterizationCachesInvalidated: (state) => { // Invalidate the rasterization caches for all entities. + const allEntities = [ + ...state.rasterLayers.entities, + ...state.controlLayers.entities, + ...state.regions.entities, + ...state.inpaintMasks.entities, + ]; - // Layers & composite layer + for (const entity of allEntities) { + entity.rasterizationCache = []; + } + + // Also invalidate the composite rasterization caches. state.rasterLayers.compositeRasterizationCache = []; - for (const layer of state.rasterLayers.entities) { - layer.rasterizationCache = []; - } - - // Regions - for (const region of state.regions.entities) { - region.rasterizationCache = []; - } - - // Inpaint mask - state.inpaintMask.rasterizationCache = []; + state.inpaintMasks.compositeRasterizationCache = []; }, canvasReset: (state) => { state.bbox = deepClone(initialState.bbox); @@ -509,16 +521,16 @@ export const canvasV2Slice = createSlice({ state.bbox.rect.height = optimalDimension; const size = pick(state.bbox.rect, 'width', 'height'); state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); + state.session = deepClone(initialState.session); + state.tool = deepClone(initialState.tool); state.ipAdapters = deepClone(initialState.ipAdapters); state.rasterLayers = deepClone(initialState.rasterLayers); - state.rasterLayers.compositeRasterizationCache = []; state.controlLayers = deepClone(initialState.controlLayers); state.regions = deepClone(initialState.regions); + state.inpaintMasks = deepClone(initialState.inpaintMasks); + state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); - state.session = deepClone(initialState.session); - state.tool = deepClone(initialState.tool); - state.inpaintMask = deepClone(initialState.inpaintMask); }, }, }); @@ -652,9 +664,11 @@ export const { loraIsEnabledChanged, loraAllDeleted, // Inpaint mask - imRecalled, - imFillColorChanged, - imFillStyleChanged, + inpaintMaskAdded, + inpaintMaskRecalled, + inpaintMaskFillColorChanged, + inpaintMaskFillStyleChanged, + inpaintMaskCompositeRasterized, // Staging sessionStartedStaging, sessionImageStaged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 03f9d60061..7ae9753b2a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,18 +1,80 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasInpaintMaskState, CanvasV2State, FillStyle, RgbaColor } from 'features/controlLayers/store/types'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { + CanvasInpaintMaskState, + CanvasV2State, + EntityIdentifierPayload, + FillStyle, + Rect, + RgbColor, +} from 'features/controlLayers/store/types'; +import { isEqual, merge } from 'lodash-es'; +import { assert } from 'tsafe'; + +export const selectInpaintMaskEntity = (state: CanvasV2State, id: string) => + state.inpaintMasks.entities.find((layer) => layer.id === id); +export const selectInpaintMaskEntityOrThrow = (state: CanvasV2State, id: string) => { + const entity = selectInpaintMaskEntity(state, id); + assert(entity, `Inpaint mask with id ${id} not found`); + return entity; +}; export const inpaintMaskReducers = { - imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { + inpaintMaskAdded: { + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; + const entity: CanvasInpaintMaskState = { + id, + name: null, + type: 'inpaint_mask', + isEnabled: true, + objects: [], + opacity: 1, + position: { x: 0, y: 0 }, + rasterizationCache: [], + fill: { + style: 'diagonal', + color: { r: 255, g: 122, b: 0 }, // some orange color + }, + }; + merge(entity, overrides); + state.inpaintMasks.entities.push(entity); + if (isSelected) { + state.selectedEntityIdentifier = { type: 'inpaint_mask', id }; + } + }, + prepare: (payload?: { overrides?: Partial; isSelected?: boolean }) => ({ + payload: { ...payload, id: getPrefixedId('inpaint_mask') }, + }), + }, + inpaintMaskRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { const { data } = action.payload; - state.inpaintMask = data; + state.inpaintMasks.entities = [data]; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - imFillColorChanged: (state, action: PayloadAction<{ color: RgbaColor }>) => { - const { color } = action.payload; - state.inpaintMask.fill.color = color; + inpaintMaskFillColorChanged: (state, action: PayloadAction>) => { + const { color, entityIdentifier } = action.payload; + const entity = selectInpaintMaskEntity(state, entityIdentifier.id); + if (!entity) { + return; + } + entity.fill.color = color; }, - imFillStyleChanged: (state, action: PayloadAction<{ style: FillStyle }>) => { - const { style } = action.payload; - state.inpaintMask.fill.style = style; + inpaintMaskFillStyleChanged: (state, action: PayloadAction>) => { + const { style, entityIdentifier } = action.payload; + const entity = selectInpaintMaskEntity(state, entityIdentifier.id); + if (!entity) { + return; + } + entity.fill.style = style; + }, + inpaintMaskCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => { + state.inpaintMasks.compositeRasterizationCache = state.inpaintMasks.compositeRasterizationCache.filter( + (cache) => !isEqual(cache.rect, action.payload.rect) + ); + state.inpaintMasks.compositeRasterizationCache.push(action.payload); }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index aebd1cb410..1714488c04 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -6,7 +6,7 @@ import type { FillStyle, IPMethodV2, RegionalGuidanceIPAdapterConfig, - RgbaColor, + RgbColor, } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; @@ -33,17 +33,17 @@ export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: st return rg; }; -const DEFAULT_MASK_COLORS: RgbaColor[] = [ - { r: 121, g: 157, b: 219, a: 0.5 }, // rgb(121, 157, 219) - { r: 131, g: 214, b: 131, a: 0.5 }, // rgb(131, 214, 131) - { r: 250, g: 225, b: 80, a: 0.5 }, // rgb(250, 225, 80) - { r: 220, g: 144, b: 101, a: 0.5 }, // rgb(220, 144, 101) - { r: 224, g: 117, b: 117, a: 0.5 }, // rgb(224, 117, 117) - { r: 213, g: 139, b: 202, a: 0.5 }, // rgb(213, 139, 202) - { r: 161, g: 120, b: 214, a: 0.5 }, // rgb(161, 120, 214) +const DEFAULT_MASK_COLORS: RgbColor[] = [ + { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) + { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) + { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) + { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) + { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) + { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) + { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) ]; -const getRGMaskFill = (state: CanvasV2State): RgbaColor => { +const getRGMaskFill = (state: CanvasV2State): RgbColor => { const lastFill = state.regions.entities.slice(-1)[0]?.fill.color; let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); if (i === -1) { @@ -69,6 +69,7 @@ export const regionsReducers = { style: 'solid', color: getRGMaskFill(state), }, + opacity: 0.5, position: { x: 0, y: 0 }, autoNegative: 'invert', positivePrompt: '', @@ -105,7 +106,7 @@ export const regionsReducers = { } entity.negativePrompt = prompt; }, - rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbaColor }>) => { + rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => { const { id, color } = action.payload; const entity = selectRegionalGuidanceEntity(state, id); if (!entity) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 369fb73f34..82bfbe052d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -41,6 +41,7 @@ import type { import { z } from 'zod'; export const zId = z.string().min(1); +export const zName = z.string().min(1).nullable(); export const zImageWithDims = z.object({ image_name: z.string(), @@ -592,7 +593,7 @@ export type IPAdapterConfig = z.infer; export const zCanvasIPAdapterState = z.object({ id: zId, - name: z.string().nullable(), + name: zName, type: z.literal('ip_adapter'), isEnabled: z.boolean(), ipAdapter: zIPAdapterConfig, @@ -665,7 +666,7 @@ export type RegionalGuidanceIPAdapterConfig = z.infer; const zCanvasInpaintMaskState = z.object({ - id: z.literal('inpaint_mask'), + id: zId, + name: zName, type: z.literal('inpaint_mask'), isEnabled: z.boolean(), position: zCoordinate, @@ -742,7 +744,7 @@ export type T2IAdapterConfig = z.infer; export const zCanvasRasterLayerState = z.object({ id: zId, - name: z.string().nullable(), + name: zName, type: z.literal('raster_layer'), isEnabled: z.boolean(), position: zCoordinate, @@ -848,7 +850,7 @@ export const isCanvasBackgroundStyle = (v: unknown): v is CanvasBackgroundStyle export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; - inpaintMask: CanvasInpaintMaskState; + inpaintMasks: { entities: CanvasInpaintMaskState[]; compositeRasterizationCache: ImageCache[] }; rasterLayers: { entities: CanvasRasterLayerState[]; compositeRasterizationCache: ImageCache[] }; controlLayers: { entities: CanvasControlLayerState[] }; ipAdapters: { entities: CanvasIPAdapterState[] }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index a1e991bc17..3aca05107d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -17,7 +17,7 @@ export const addImageToImage = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const { image_name } = await manager.getCompositeLayerImageDTO(bbox.rect); + const { image_name } = await manager.getCompositeRasterLayerImageDTO(bbox.rect); if (!isEqual(scaledSize, originalSize)) { // Resize the initial image to the scaled size, denoise, then resize back to the original size diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index d36a2b8b17..c18cbf672f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -21,8 +21,8 @@ export const addInpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.inpaintMaskAdapter.renderer.rasterize(bbox.rect); + const initialImage = await manager.getCompositeRasterLayerImageDTO(bbox.rect); + const maskImage = await manager.getCompositeInpaintMaskImageDTO(bbox.rect); if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 118fb779a2..0bc2b40cd9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -22,8 +22,8 @@ export const addOutpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.inpaintMaskAdapter.renderer.rasterize(bbox.rect); + const initialImage = await manager.getCompositeRasterLayerImageDTO(bbox.rect); + const maskImage = await manager.getCompositeInpaintMaskImageDTO(bbox.rect); const infill = getInfill(g, compositing); if (!isEqual(scaledSize, originalSize)) {