From f1348e45bd5fd7cfba0c396f03bb19912d2f82bd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:57:03 +1000 Subject: [PATCH] feat(ui): revised rasterization caching - use `stable-hash` to generate stable, non-crypto hashes for cache entries, instead of using deep object comparisons - use an object to store image name caches --- invokeai/frontend/web/package.json | 1 + invokeai/frontend/web/pnpm-lock.yaml | 7 +++ .../controlLayers/konva/CanvasManager.ts | 44 +++++++------- .../konva/CanvasObjectRenderer.ts | 20 +++---- .../controlLayers/konva/CanvasStateApi.ts | 4 +- .../src/features/controlLayers/konva/util.ts | 3 + .../controlLayers/store/canvasV2Slice.ts | 57 +++++++++++-------- .../store/controlLayersReducers.ts | 4 +- .../store/inpaintMaskReducers.ts | 13 ++--- .../store/rasterLayersReducers.ts | 22 ++++--- .../controlLayers/store/regionsReducers.ts | 2 +- .../src/features/controlLayers/store/types.ts | 36 +++++++----- 12 files changed, 116 insertions(+), 97 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index f5d52f195f..a75a9bebcc 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -103,6 +103,7 @@ "roarr": "^7.21.1", "serialize-error": "^11.0.3", "socket.io-client": "^4.7.5", + "stable-hash": "^0.0.4", "use-debounce": "^10.0.2", "use-device-pixel-ratio": "^1.1.2", "uuid": "^10.0.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index e3d1ea19ee..532db45136 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -158,6 +158,9 @@ dependencies: socket.io-client: specifier: ^4.7.5 version: 4.7.5 + stable-hash: + specifier: ^0.0.4 + version: 0.0.4 use-debounce: specifier: ^10.0.2 version: 10.0.2(react@18.3.1) @@ -10595,6 +10598,10 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true + /stable-hash@0.0.4: + resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + dev: false + /stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} dependencies: diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 4e942234ed..0b57c26ff3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -7,6 +7,7 @@ import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva import { canvasToBlob, canvasToImageData, + getHash, getImageDataTransparency, getPrefixedId, getRectUnion, @@ -14,16 +15,9 @@ import { previewBlob, } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; -import type { - CanvasV2State, - Coordinate, - Dimensions, - GenerationMode, - ImageCache, - Rect, -} from 'features/controlLayers/store/types'; +import type { CanvasV2State, Coordinate, Dimensions, GenerationMode, Rect } from 'features/controlLayers/store/types'; import type Konva from 'konva'; -import { clamp, isEqual } from 'lodash-es'; +import { clamp } from 'lodash-es'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; @@ -635,26 +629,25 @@ export class CanvasManager { return canvas; }; - getCompositeInpaintMaskImageCache = (rect: Rect): ImageCache | null => { + getCompositeInpaintMaskImageCache = (hash: string): string | null => { const { compositeRasterizationCache } = this.stateApi.getInpaintMasksState(); - const imageCache = compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); - return imageCache ?? null; + return compositeRasterizationCache[hash] ?? null; }; - getCompositeRasterLayerImageCache = (rect: Rect): ImageCache | null => { + getCompositeRasterLayerImageCache = (hash: string): string | null => { const { compositeRasterizationCache } = this.stateApi.getRasterLayersState(); - const imageCache = compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); - return imageCache ?? null; + return compositeRasterizationCache[hash] ?? null; }; getCompositeRasterLayerImageDTO = async (rect: Rect): Promise => { let imageDTO: ImageDTO | null = null; - const compositeRasterizedImageCache = this.getCompositeRasterLayerImageCache(rect); + const hash = getHash(rect); + const cachedImageName = this.getCompositeRasterLayerImageCache(hash); - if (compositeRasterizedImageCache) { - imageDTO = await getImageDTO(compositeRasterizedImageCache.imageName); + if (cachedImageName) { + imageDTO = await getImageDTO(cachedImageName); if (imageDTO) { - this.log.trace({ rect, compositeRasterizedImageCache, imageDTO }, 'Using cached composite raster layer image'); + this.log.trace({ rect, imageName: cachedImageName, imageDTO }, 'Using cached composite raster layer image'); return imageDTO; } } @@ -668,18 +661,19 @@ export class CanvasManager { } imageDTO = await uploadImage(blob, 'composite-raster-layer.png', 'general', true); - this.stateApi.compositeRasterLayerRasterized({ imageName: imageDTO.image_name, rect }); + this.stateApi.compositeRasterLayerRasterized({ imageName: imageDTO.image_name, hash }); return imageDTO; }; getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise => { let imageDTO: ImageDTO | null = null; - const compositeRasterizedImageCache = this.getCompositeInpaintMaskImageCache(rect); + const hash = getHash(rect); + const cachedImageName = this.getCompositeInpaintMaskImageCache(hash); - if (compositeRasterizedImageCache) { - imageDTO = await getImageDTO(compositeRasterizedImageCache.imageName); + if (cachedImageName) { + imageDTO = await getImageDTO(cachedImageName); if (imageDTO) { - this.log.trace({ rect, compositeRasterizedImageCache, imageDTO }, 'Using cached composite inpaint mask image'); + this.log.trace({ rect, cachedImageName, imageDTO }, 'Using cached composite inpaint mask image'); return imageDTO; } } @@ -693,7 +687,7 @@ export class CanvasManager { } imageDTO = await uploadImage(blob, 'composite-inpaint-mask.png', 'general', true); - this.stateApi.compositeInpaintMaskRasterized({ imageName: imageDTO.image_name, rect }); + this.stateApi.compositeInpaintMaskRasterized({ imageName: imageDTO.image_name, hash }); return imageDTO; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index fea565340f..5fe137a30f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -10,6 +10,7 @@ import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; import { + getHash, getPrefixedId, konvaNodeToBlob, konvaNodeToCanvas, @@ -22,13 +23,11 @@ import type { CanvasImageState, CanvasRectState, Fill, - ImageCache, Rect, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; -import { isEqual } from 'lodash-es'; import type { Logger } from 'roarr'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; @@ -485,9 +484,8 @@ export class CanvasObjectRenderer { return this.renderers.size > 0 || this.bufferState !== null || this.bufferRenderer !== null; }; - getRasterizedImageCache = (rect: Rect): ImageCache | null => { - const imageCache = this.parent.state.rasterizationCache.find((cache) => isEqual(cache.rect, rect)); - return imageCache ?? null; + getRasterizedImageCache = (hash: string): string | null => { + return this.parent.state.rasterizationCache[hash] ?? null; }; /** @@ -500,14 +498,15 @@ export class CanvasObjectRenderer { * @param rect The rect to rasterize. If omitted, the entity's full rect will be used. * @returns A promise that resolves to the rasterized image DTO. */ - rasterize = async (rect: Rect, replaceObjects: boolean = false): Promise => { + rasterize = async (rect: Rect, replaceObjects: boolean = false, attrs?: GroupConfig): Promise => { let imageDTO: ImageDTO | null = null; - const rasterizedImageCache = this.getRasterizedImageCache(rect); + const hash = getHash({ rect, attrs }); + const cachedImageName = this.getRasterizedImageCache(hash); - if (rasterizedImageCache) { - imageDTO = await getImageDTO(rasterizedImageCache.imageName); + if (cachedImageName) { + imageDTO = await getImageDTO(cachedImageName); if (imageDTO) { - this.log.trace({ rect, rasterizedImageCache, imageDTO }, 'Using cached rasterized image'); + this.log.trace({ rect, cachedImageName, imageDTO }, 'Using cached rasterized image'); return imageDTO; } } @@ -527,6 +526,7 @@ export class CanvasObjectRenderer { this.manager.stateApi.rasterizeEntity({ entityIdentifier: this.parent.getEntityIdentifier(), imageObject, + hash, rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: imageDTO.width, height: imageDTO.height }, replaceObjects, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index c90b9d12c0..cc3b8fc0c3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -108,10 +108,10 @@ export class CanvasStateApi { rasterizeEntity = (arg: EntityRasterizedPayload) => { this._store.dispatch(entityRasterized(arg)); }; - compositeRasterLayerRasterized = (arg: { imageName: string; rect: Rect }) => { + compositeRasterLayerRasterized = (arg: { hash: string; imageName: string }) => { this._store.dispatch(rasterLayerCompositeRasterized(arg)); }; - compositeInpaintMaskRasterized = (arg: { imageName: string; rect: Rect }) => { + compositeInpaintMaskRasterized = (arg: { hash: string; imageName: string }) => { this._store.dispatch(inpaintMaskCompositeRasterized(arg)); }; setSelectedEntity = (arg: EntityIdentifierPayload) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index e1e66ac07d..55ec3345a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -3,6 +3,7 @@ import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; import { customAlphabet } from 'nanoid'; +import stableHash from 'stable-hash'; import { assert } from 'tsafe'; /** @@ -398,3 +399,5 @@ export const getRectUnion = (...rects: Rect[]): Rect => { }, getEmptyRect()); return rect; }; + +export const getHash = stableHash; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index df1610c4cf..e79fc113dc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -45,12 +45,22 @@ import { getEntityIdentifier, isDrawableEntity } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, - rasterLayers: { entities: [], compositeRasterizationCache: [] }, - controlLayers: { entities: [] }, - ipAdapters: { entities: [] }, - regions: { entities: [] }, + rasterLayers: { + entities: [], + compositeRasterizationCache: {}, + }, + controlLayers: { + entities: [], + }, + inpaintMasks: { + entities: [], + compositeRasterizationCache: {}, + }, + regions: { + entities: [], + }, loras: [], - inpaintMasks: { entities: [], compositeRasterizationCache: [] }, + ipAdapters: { entities: [] }, tool: { selected: 'view', selectedBuffer: null, @@ -170,16 +180,16 @@ const invalidateRasterizationCaches = ( // cached rect. // Reset the entity's rasterization cache - entity.rasterizationCache = []; + entity.rasterizationCache = {}; // When an individual layer has its cache reset, we must also reset the composite rasterization cache because the // layer's image data will contribute to the composite layer's image data. // If the layer is used as a control layer, it will not contribute to the composite layer, so we do not need to reset // its cache. if (entity.type === 'raster_layer') { - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; } else if (entity.type === 'inpaint_mask') { - state.inpaintMasks.compositeRasterizationCache = []; + state.inpaintMasks.compositeRasterizationCache = {}; } }; @@ -247,7 +257,7 @@ export const canvasV2Slice = createSlice({ } }, entityRasterized: (state, action: PayloadAction) => { - const { entityIdentifier, imageObject, rect, replaceObjects } = action.payload; + const { entityIdentifier, imageObject, hash, rect, replaceObjects } = action.payload; const entity = selectEntity(state, entityIdentifier); if (!entity) { return; @@ -256,8 +266,7 @@ export const canvasV2Slice = createSlice({ if (isDrawableEntity(entity)) { // Remove the cache for the given rect. This should never happen, because we should never rasterize the same // rect twice. Just in case, we remove the old cache. - entity.rasterizationCache = entity.rasterizationCache.filter((cache) => !isEqual(cache.rect, rect)); - entity.rasterizationCache.push({ imageName: imageObject.image.image_name, rect }); + entity.rasterizationCache[hash] = imageObject.image.image_name; if (replaceObjects) { entity.objects = [imageObject]; @@ -323,7 +332,7 @@ export const canvasV2Slice = createSlice({ const index = state.rasterLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); // When deleting a raster layer, we need to invalidate the composite rasterization cache. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; const nextRasterLayer = state.rasterLayers.entities[index]; if (nextRasterLayer) { selectedEntityIdentifier = { type: nextRasterLayer.type, id: nextRasterLayer.id }; @@ -353,7 +362,7 @@ export const canvasV2Slice = createSlice({ 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 = []; + state.inpaintMasks.compositeRasterizationCache = {}; const entity = state.inpaintMasks.entities[index]; if (entity) { selectedEntityIdentifier = { type: entity.type, id: entity.id }; @@ -373,7 +382,7 @@ export const canvasV2Slice = createSlice({ if (entity.type === 'raster_layer') { moveOneToEnd(state.rasterLayers.entities, entity); // When arranging a raster layer, we need to invalidate the composite rasterization cache. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; } else if (entity.type === 'control_layer') { moveOneToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { @@ -381,7 +390,7 @@ export const canvasV2Slice = createSlice({ } 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 = []; + state.inpaintMasks.compositeRasterizationCache = {}; } }, entityArrangedToFront: (state, action: PayloadAction) => { @@ -393,7 +402,7 @@ export const canvasV2Slice = createSlice({ if (entity.type === 'raster_layer') { moveToEnd(state.rasterLayers.entities, entity); // When arranging a raster layer, we need to invalidate the composite rasterization cache. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; } else if (entity.type === 'control_layer') { moveToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { @@ -401,7 +410,7 @@ export const canvasV2Slice = createSlice({ } 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 = []; + state.inpaintMasks.compositeRasterizationCache = {}; } }, entityArrangedBackwardOne: (state, action: PayloadAction) => { @@ -413,7 +422,7 @@ export const canvasV2Slice = createSlice({ if (entity.type === 'raster_layer') { moveOneToStart(state.rasterLayers.entities, entity); // When arranging a raster layer, we need to invalidate the composite rasterization cache. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; } else if (entity.type === 'control_layer') { moveOneToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { @@ -421,7 +430,7 @@ export const canvasV2Slice = createSlice({ } 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 = []; + state.inpaintMasks.compositeRasterizationCache = {}; } }, entityArrangedToBack: (state, action: PayloadAction) => { @@ -433,7 +442,7 @@ export const canvasV2Slice = createSlice({ if (entity.type === 'raster_layer') { moveToStart(state.rasterLayers.entities, entity); // When arranging a raster layer, we need to invalidate the composite rasterization cache. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; } else if (entity.type === 'control_layer') { moveToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { @@ -441,7 +450,7 @@ export const canvasV2Slice = createSlice({ } 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 = []; + state.inpaintMasks.compositeRasterizationCache = {}; } }, entityOpacityChanged: (state, action: PayloadAction>) => { @@ -506,12 +515,12 @@ export const canvasV2Slice = createSlice({ ]; for (const entity of allEntities) { - entity.rasterizationCache = []; + entity.rasterizationCache = {}; } // Also invalidate the composite rasterization caches. - state.rasterLayers.compositeRasterizationCache = []; - state.inpaintMasks.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; + state.inpaintMasks.compositeRasterizationCache = {}; }, canvasReset: (state) => { state.bbox = deepClone(initialState.bbox); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts index 31628f4e71..36ac9a1d73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts @@ -40,7 +40,7 @@ export const controlLayersReducers = { objects: [], opacity: 1, position: { x: 0, y: 0 }, - rasterizationCache: [], + rasterizationCache: {}, controlAdapter: deepClone(initialControlNetV2), }; merge(layer, overrides); @@ -83,7 +83,7 @@ export const controlLayersReducers = { state.rasterLayers.entities.push(rasterLayerState); // The composite layer's image data will change when the control layer is converted to raster layer. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 7ae9753b2a..e7244fd818 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -5,10 +5,9 @@ import type { CanvasV2State, EntityIdentifierPayload, FillStyle, - Rect, RgbColor, } from 'features/controlLayers/store/types'; -import { isEqual, merge } from 'lodash-es'; +import { merge } from 'lodash-es'; import { assert } from 'tsafe'; export const selectInpaintMaskEntity = (state: CanvasV2State, id: string) => @@ -34,7 +33,7 @@ export const inpaintMaskReducers = { objects: [], opacity: 1, position: { x: 0, y: 0 }, - rasterizationCache: [], + rasterizationCache: {}, fill: { style: 'diagonal', color: { r: 255, g: 122, b: 0 }, // some orange color @@ -71,10 +70,8 @@ export const inpaintMaskReducers = { } 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); + inpaintMaskCompositeRasterized: (state, action: PayloadAction<{ hash: string; imageName: string }>) => { + const { hash, imageName } = action.payload; + state.inpaintMasks.compositeRasterizationCache[hash] = imageName; }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts index 67af35c3f3..edfd491573 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -1,10 +1,10 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { isEqual, merge } from 'lodash-es'; +import { merge } from 'lodash-es'; import { assert } from 'tsafe'; -import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State, Rect } from './types'; +import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State } from './types'; import { initialControlNetV2 } from './types'; export const selectRasterLayer = (state: CanvasV2State, id: string) => @@ -30,7 +30,7 @@ export const rasterLayersReducers = { objects: [], opacity: 1, position: { x: 0, y: 0 }, - rasterizationCache: [], + rasterizationCache: {}, }; merge(layer, overrides); state.rasterLayers.entities.push(layer); @@ -40,7 +40,7 @@ export const rasterLayersReducers = { if (layer.objects.length > 0) { // This new layer will change the composite layer's image data. Invalidate the cache. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; } }, prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ @@ -53,18 +53,16 @@ export const rasterLayersReducers = { state.selectedEntityIdentifier = { type: 'raster_layer', id: data.id }; if (data.objects.length > 0) { // This new layer will change the composite layer's image data. Invalidate the cache. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; } }, rasterLayerAllDeleted: (state) => { state.rasterLayers.entities = []; - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; }, - rasterLayerCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => { - state.rasterLayers.compositeRasterizationCache = state.rasterLayers.compositeRasterizationCache.filter( - (cache) => !isEqual(cache.rect, action.payload.rect) - ); - state.rasterLayers.compositeRasterizationCache.push(action.payload); + rasterLayerCompositeRasterized: (state, action: PayloadAction<{ hash: string; imageName: string }>) => { + const { hash, imageName } = action.payload; + state.rasterLayers.compositeRasterizationCache[hash] = imageName; }, rasterLayerConvertedToControlLayer: { reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => { @@ -90,7 +88,7 @@ export const rasterLayersReducers = { state.controlLayers.entities.push(controlLayerState); // The composite layer's image data will change when the raster layer is converted to control layer. - state.rasterLayers.compositeRasterizationCache = []; + state.rasterLayers.compositeRasterizationCache = {}; state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 1714488c04..2d69b9b5ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -75,7 +75,7 @@ export const regionsReducers = { positivePrompt: '', negativePrompt: null, ipAdapters: [], - rasterizationCache: [], + rasterizationCache: {}, }; state.regions.entities.push(rg); state.selectedEntityIdentifier = { type: 'regional_guidance', id }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 8ccdc3f6ba..47bc392646 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -648,10 +648,7 @@ export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse( const zFill = z.object({ style: zFillStyle, color: zRgbColor }); export type Fill = z.infer; -const zImageCache = z.object({ - imageName: z.string(), - rect: zRect, -}); +const zImageCache = z.record(z.string()).default({}); export type ImageCache = z.infer; const zRegionalGuidanceIPAdapterConfig = z.object({ @@ -678,7 +675,7 @@ export const zCanvasRegionalGuidanceState = z.object({ negativePrompt: zParameterNegativePrompt.nullable(), ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig), autoNegative: zAutoNegative, - rasterizationCache: z.array(zImageCache), + rasterizationCache: zImageCache, }); export type CanvasRegionalGuidanceState = z.infer; @@ -691,7 +688,7 @@ const zCanvasInpaintMaskState = z.object({ fill: zFill, opacity: zOpacity, objects: z.array(zCanvasObjectState), - rasterizationCache: z.array(zImageCache), + rasterizationCache: zImageCache, }); export type CanvasInpaintMaskState = z.infer; @@ -751,7 +748,7 @@ export const zCanvasRasterLayerState = z.object({ position: zCoordinate, opacity: zOpacity, objects: z.array(zCanvasObjectState), - rasterizationCache: z.array(zImageCache), + rasterizationCache: zImageCache, }); export type CanvasRasterLayerState = z.infer; @@ -851,11 +848,23 @@ export const isCanvasBackgroundStyle = (v: unknown): v is CanvasBackgroundStyle export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; - inpaintMasks: { entities: CanvasInpaintMaskState[]; compositeRasterizationCache: ImageCache[] }; - rasterLayers: { entities: CanvasRasterLayerState[]; compositeRasterizationCache: ImageCache[] }; - controlLayers: { entities: CanvasControlLayerState[] }; - ipAdapters: { entities: CanvasIPAdapterState[] }; - regions: { entities: CanvasRegionalGuidanceState[] }; + inpaintMasks: { + entities: CanvasInpaintMaskState[]; + compositeRasterizationCache: ImageCache; + }; + rasterLayers: { + entities: CanvasRasterLayerState[]; + compositeRasterizationCache: ImageCache; + }; + controlLayers: { + entities: CanvasControlLayerState[]; + }; + regions: { + entities: CanvasRegionalGuidanceState[]; + }; + ipAdapters: { + entities: CanvasIPAdapterState[]; + }; loras: LoRA[]; tool: { selected: Tool; @@ -952,8 +961,9 @@ export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: C export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{ eraserLine: CanvasEraserLineState }>; export type EntityRectAddedPayload = EntityIdentifierPayload<{ rect: CanvasRectState }>; export type EntityRasterizedPayload = EntityIdentifierPayload<{ + hash: string; imageObject: CanvasImageState; - rect: Rect; + rect: Rect, replaceObjects: boolean; }>; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate };