diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts index 2b45f61b291..c733334ed09 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts @@ -546,6 +546,14 @@ export abstract class CanvasEntityAdapterBase { + this.renderer.invalidateRasterCache(); + }; + /** * Synchronizes the entity's locked state with the canvas. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts index 8bb8ec25319..57aea0630cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts @@ -65,6 +65,11 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase { */ renderers = new SyncableMap(); + /** + * Tracks the cache keys used when rasterizing this entity so they can be invalidated on demand. + */ + rasterCacheKeys = new Set(); + /** * A object containing singleton Konva nodes. */ @@ -476,7 +481,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase { }): Promise => { const rasterizingAdapter = this.manager.stateApi.$rasterizingAdapter.get(); if (rasterizingAdapter) { - assert(false, `Already rasterizing an entity: ${rasterizingAdapter.id}`); + await this.manager.stateApi.waitForRasterizationToFinish(); } const { rect, replaceObjects, attrs, bg, ignoreCache } = { @@ -493,6 +498,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase { if (cachedImageName && !ignoreCache) { imageDTO = await getImageDTOSafe(cachedImageName); if (imageDTO) { + this.rasterCacheKeys.add(hash); this.log.trace({ rect, cachedImageName, imageDTO }, 'Using cached rasterized image'); return imageDTO; } @@ -524,6 +530,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase { replaceObjects, }); this.manager.cache.imageNameCache.set(hash, imageDTO.image_name); + this.rasterCacheKeys.add(hash); return imageDTO; } catch (error) { this.log.error({ rasterizeArgs, error: serializeError(error as Error) }, 'Failed to rasterize entity'); @@ -533,6 +540,22 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase { } }; + /** + * Invalidates all cached rasterizations for this entity by removing the cached image + * names from the image cache and clearing the tracked raster cache keys. This forces + * future rasterizations to regenerate images instead of using potentially stale + * cached versions. + */ + invalidateRasterCache = () => { + if (this.rasterCacheKeys.size === 0) { + return; + } + for (const key of this.rasterCacheKeys) { + this.manager.cache.imageNameCache.delete(key); + } + this.rasterCacheKeys.clear(); + }; + cloneObjectGroup = (arg: { attrs?: GroupConfig } = {}): Konva.Group => { const { attrs } = arg; const clone = this.konva.objectGroup.clone(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 38248136625..4b210cf327a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -199,6 +199,17 @@ export class CanvasManager extends CanvasModuleBase { ]; }; + /** + * Invalidates the raster cache for all regional guidance adapters. + * This should be called when bbox or related regional guidance settings change + * to ensure that cached masks are regenerated with the new settings. + */ + invalidateRegionalGuidanceRasterCache = () => { + for (const adapter of this.adapters.regionMasks.values()) { + adapter.invalidateRasterCache(); + } + }; + createAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter => { if (isRasterLayerEntityIdentifier(entityIdentifier)) { const adapter = new CanvasEntityAdapterRasterLayer(entityIdentifier, this); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 57027aaa8f9..dc2c0b15043 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -213,6 +213,52 @@ export class CanvasStateApiModule extends CanvasModuleBase { */ setGenerationBbox = (rect: Rect) => { this.store.dispatch(bboxChangedFromCanvas(rect)); + this.manager.invalidateRegionalGuidanceRasterCache(); + }; + + /** + * Waits for the current rasterization operation to complete. + * + * If no rasterization is in progress, this returns immediately. Use this + * before starting a new rasterization to avoid multiple simultaneous + * rasterization operations acting on the same canvas state. + * + * @returns A promise that resolves once rasterization has finished or + * immediately if no rasterization is in progress. + */ + waitForRasterizationToFinish = async () => { + if (!this.$rasterizingAdapter.get()) { + return; + } + + await new Promise((resolve) => { + // Ensure we only resolve once, even if multiple events fire. + let resolved = false; + + // Re-check before subscribing to avoid a race where rasterization completes + // between the outer check and listener registration. + if (!this.$rasterizingAdapter.get()) { + resolved = true; + resolve(); + return; + } + + const unsubscribe = this.$rasterizingAdapter.listen((adapter) => { + if (!adapter && !resolved) { + resolved = true; + unsubscribe(); + resolve(); + } + }); + + // Re-check immediately after subscribing to close the race where + // rasterization completes between the check above and `listen()`. + if (!this.$rasterizingAdapter.get() && !resolved) { + resolved = true; + unsubscribe(); + resolve(); + } + }); }; /**