Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,14 @@ export abstract class CanvasEntityAdapterBase<T extends CanvasEntityState, U ext
this.renderer.syncKonvaCache();
};

/**
* Invalidates the raster cache for this entity by delegating to the renderer's
* `invalidateRasterCache` method.
*/
invalidateRasterCache = () => {
this.renderer.invalidateRasterCache();
};

/**
* Synchronizes the entity's locked state with the canvas.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
*/
renderers = new SyncableMap<string, AnyObjectRenderer>();

/**
* Tracks the cache keys used when rasterizing this entity so they can be invalidated on demand.
*/
rasterCacheKeys = new Set<string>();

/**
* A object containing singleton Konva nodes.
*/
Expand Down Expand Up @@ -476,7 +481,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
}): Promise<ImageDTO> => {
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 } = {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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');
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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();
}
});
};

/**
Expand Down