refactor(ui): generalize compositor methods

`CanvasCompositorModule` had a fairly inflexible API, only supporting compositing all raster layers or inpaint masks.

The API has been generalized work with a list of canvas entities. This enables `Merge Down` and `Merge Selected` functionality (though `Merge Selected` is not part of this set of changes).
This commit is contained in:
psychedelicious
2024-10-29 15:42:10 +10:00
parent bc42205593
commit 91db9c9300
8 changed files with 210 additions and 309 deletions

View File

@@ -1,14 +1,17 @@
import { IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import type { AppDispatch } from 'app/store/store';
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 type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { CanvasEntityType } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackBold } from 'react-icons/pi';
@@ -17,7 +20,61 @@ import { serializeError } from 'serialize-error';
const log = logger('canvas');
type Props = {
type: CanvasEntityIdentifier['type'];
type: CanvasEntityType;
};
const mergeRasterLayers = async (canvasManager: CanvasManager, dispatch: AppDispatch) => {
const rect = canvasManager.stage.getVisibleRect('raster_layer');
const adapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
const result = await withResultAsync(() =>
canvasManager.compositor.getCompositeImageDTO(adapters, rect, { is_intermediate: true })
);
if (result.isErr()) {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
return;
}
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') });
};
const mergeInpaintMasks = async (canvasManager: CanvasManager, dispatch: AppDispatch) => {
const rect = canvasManager.stage.getVisibleRect('inpaint_mask');
const adapters = canvasManager.compositor.getVisibleAdaptersOfType('inpaint_mask');
const result = await withResultAsync(() =>
canvasManager.compositor.getCompositeImageDTO(adapters, rect, { is_intermediate: true })
);
if (result.isErr()) {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
return;
}
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') });
};
export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
@@ -26,55 +83,18 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
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');
const onClick = useCallback(() => {
switch (type) {
case 'raster_layer':
mergeRasterLayers(canvasManager, dispatch);
break;
case 'inpaint_mask':
mergeInpaintMasks(canvasManager, dispatch);
break;
default:
log.error({ type }, 'Unsupported type for merge visible');
}
}, [canvasManager.compositor, canvasManager.stage, dispatch, t, type]);
}, [canvasManager, dispatch, type]);
return (
<IconButton

View File

@@ -68,12 +68,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) {

View File

@@ -1,5 +1,6 @@
import type { SerializableObject } from 'common/types';
import { withResultAsync } from 'common/util/result';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import {
@@ -9,7 +10,13 @@ import {
getPrefixedId,
previewBlob,
} from 'features/controlLayers/konva/util';
import type { GenerationMode, Rect } from 'features/controlLayers/store/types';
import type {
CanvasEntityState,
CanvasRenderableEntityType,
GenerationMode,
Rect,
} from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { atom, computed } from 'nanostores';
import type { Logger } from 'roarr';
@@ -54,41 +61,53 @@ export class CanvasCompositorModule extends CanvasModuleBase {
}
/**
* Gets the entity IDs of all raster layers that should be included in the composite raster layer.
* A raster layer is included if it is enabled and has objects. The ids are sorted by draw order.
* @returns An array of raster layer entity IDs
* Gets all visible adapters for the given entity type. Visible adapters are those that are not disabled and have
* objects to render. This is used for "merge visible" functionality and for calculating the generation mode.
*
* This includes all adapters that are not disabled and have objects to render.
*
* @param type The entity type
* @returns The adapters for the given entity type that are eligible to be included in a composite
*/
getCompositeRasterLayerEntityIds = (): string[] => {
const validSortedIds = [];
const sortedIds = this.manager.stateApi.getRasterLayersState().entities.map(({ id }) => id);
for (const id of sortedIds) {
const adapter = this.manager.adapters.rasterLayers.get(id);
if (!adapter) {
this.log.warn({ id }, 'Raster layer adapter not found');
continue;
}
if (adapter.state.isEnabled && adapter.state.objects.length > 0) {
validSortedIds.push(adapter.id);
}
getVisibleAdaptersOfType = <T extends CanvasRenderableEntityType>(
type: T
): Extract<CanvasEntityAdapter, { state: { type: T } }>[] => {
let entities: CanvasEntityState[];
switch (type) {
case 'raster_layer':
entities = this.manager.stateApi.getRasterLayersState().entities;
break;
case 'inpaint_mask':
entities = this.manager.stateApi.getInpaintMasksState().entities;
break;
case 'control_layer':
entities = this.manager.stateApi.getControlLayersState().entities;
break;
case 'regional_guidance':
entities = this.manager.stateApi.getRegionsState().entities;
break;
default:
assert(false, `Unhandled entity type: ${type}`);
}
return validSortedIds;
const adapters: CanvasEntityAdapter[] = entities
// Get the identifier for each entity
.map((entity) => getEntityIdentifier(entity))
// Get the adapter for each entity
.map(this.manager.getAdapter)
// Filter out null adapters
.filter((adapter) => !!adapter)
// Filter out adapters that are disabled or have no objects (and are thus not to be included in the composite)
.filter((adapter) => !adapter.$isDisabled.get() && adapter.renderer.hasObjects());
return adapters as Extract<CanvasEntityAdapter, { state: { type: T } }>[];
};
/**
* Gets a hash of the composite raster layer, which includes the state of all raster layers that are included in the
* composite plus arbitrary extra data that should contribute to the hash (e.g. a rect).
* @param extra Any extra data to include in the hash
* @returns A hash for the composite raster layer
*/
getCompositeRasterLayerHash = (extra: SerializableObject): string => {
getCompositeHash = (adapters: CanvasEntityAdapter[], extra: SerializableObject): string => {
const adapterHashes: SerializableObject[] = [];
for (const id of this.getCompositeRasterLayerEntityIds()) {
const adapter = this.manager.adapters.rasterLayers.get(id);
if (!adapter) {
this.log.warn({ id }, 'Raster layer adapter not found');
continue;
}
for (const adapter of adapters) {
adapterHashes.push(adapter.getHashableState());
}
@@ -101,23 +120,28 @@ export class CanvasCompositorModule extends CanvasModuleBase {
};
/**
* Gets a canvas element for the composite raster layer. Only the region defined by the rect is included in the canvas.
* Composites the given canvas entities for the given rect and returns the resulting canvas.
*
* If the hash of the composite raster layer is found in the cache, the cached canvas is returned.
* The canvas element is cached to avoid recomputing it when the canvas state has not changed.
*
* The canvas entities are drawn in the order they are provided.
*
* @param adapters The adapters for the canvas entities to composite, in the order they should be drawn
* @param rect The region to include in the canvas
* @returns A canvas element with the composite raster layer drawn on it
* @returns The composite canvas
*/
getCompositeRasterLayerCanvas = (rect: Rect): HTMLCanvasElement => {
const hash = this.getCompositeRasterLayerHash({ rect });
getCompositeCanvas = (adapters: CanvasEntityAdapter[], rect: Rect): HTMLCanvasElement => {
const entityIdentifiers = adapters.map((adapter) => adapter.entityIdentifier);
const hash = this.getCompositeHash(adapters, { rect });
const cachedCanvas = this.manager.cache.canvasElementCache.get(hash);
if (cachedCanvas) {
this.log.trace({ rect }, 'Using cached composite raster layer canvas');
this.log.debug({ entityIdentifiers, rect }, 'Using cached composite canvas');
return cachedCanvas;
}
this.log.trace({ rect }, 'Building composite raster layer canvas');
this.log.debug({ entityIdentifiers, rect }, 'Building composite canvas');
this.$isCompositing.set(true);
const canvas = document.createElement('canvas');
@@ -129,13 +153,8 @@ export class CanvasCompositorModule extends CanvasModuleBase {
ctx.imageSmoothingEnabled = false;
for (const id of this.getCompositeRasterLayerEntityIds()) {
const adapter = this.manager.adapters.rasterLayers.get(id);
if (!adapter) {
this.log.warn({ id }, 'Raster layer adapter not found');
continue;
}
this.log.trace({ id }, 'Drawing raster layer to composite canvas');
for (const adapter of adapters) {
this.log.debug({ entityIdentifier: adapter.entityIdentifier }, 'Drawing entity to composite canvas');
const adapterCanvas = adapter.getCanvas(rect);
ctx.drawImage(adapterCanvas, 0, 0);
}
@@ -145,23 +164,40 @@ export class CanvasCompositorModule extends CanvasModuleBase {
};
/**
* Rasterizes the composite raster layer and uploads it to the server.
* Composites the given canvas entities for the given rect and uploads the resulting image.
*
* If the hash of the composite raster layer is found in the cache, the cached image DTO is returned.
* The uploaded image is cached to avoid recomputing it when the canvas state has not changed. The canvas elements
* created for each entity are also cached to avoid recomputing them when the canvas state has not changed.
*
* The canvas entities are drawn in the order they are provided.
*
* @param adapters The adapters for the canvas entities to composite, in the order they should be drawn
* @param rect The region to include in the rasterized image
* @param options Options for uploading the image
* @returns A promise that resolves to the uploaded image DTO
* @returns A promise that resolves to the image DTO
*/
rasterizeAndUploadCompositeRasterLayer = async (
getCompositeImageDTO = async (
adapters: CanvasEntityAdapter[],
rect: Rect,
options: Pick<UploadOptions, 'is_intermediate' | 'metadata'>
): Promise<ImageDTO> => {
this.log.trace({ rect }, 'Rasterizing composite raster layer');
assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect');
const canvas = this.getCompositeRasterLayerCanvas(rect);
const hash = this.getCompositeHash(adapters, { rect });
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
let imageDTO: ImageDTO | null = null;
if (cachedImageName) {
imageDTO = await getImageDTOSafe(cachedImageName);
if (imageDTO) {
this.log.debug({ rect, imageName: cachedImageName, imageDTO }, 'Using cached composite image');
return imageDTO;
}
this.log.warn({ rect, imageName: cachedImageName }, 'Cached image name not found, recompositing');
}
const canvas = this.getCompositeCanvas(adapters, rect);
this.$isProcessing.set(true);
const blobResult = await withResultAsync(() => canvasToBlob(canvas));
@@ -173,14 +209,14 @@ export class CanvasCompositorModule extends CanvasModuleBase {
const blob = blobResult.value;
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite raster layer canvas');
previewBlob(blob, 'Composite');
}
this.$isUploading.set(true);
const uploadResult = await withResultAsync(() =>
uploadImage({
blob,
fileName: 'composite-raster-layer.png',
fileName: 'canvas-composite.png',
image_category: 'general',
is_intermediate: options.is_intermediate,
board_id: options.is_intermediate ? undefined : selectAutoAddBoardId(this.manager.store.getState()),
@@ -191,197 +227,7 @@ export class CanvasCompositorModule extends CanvasModuleBase {
if (uploadResult.isErr()) {
throw uploadResult.error;
}
const imageDTO = uploadResult.value;
return imageDTO;
};
/**
* Gets the image DTO for the composite raster layer.
*
* If the image is found in the cache, the cached image DTO is returned.
*
* @param rect The region to include in the image
* @returns A promise that resolves to the image DTO
*/
getCompositeRasterLayerImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
const hash = this.getCompositeRasterLayerHash({ rect });
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
if (cachedImageName) {
imageDTO = await getImageDTOSafe(cachedImageName);
if (imageDTO) {
this.log.trace({ rect, imageName: cachedImageName, imageDTO }, 'Using cached composite raster layer image');
return imageDTO;
}
}
imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, { is_intermediate: true });
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
/**
* Gets the entity IDs of all inpaint masks that should be included in the composite inpaint mask.
* An inpaint mask is included if it is enabled and has objects. The ids are sorted by draw order.
* @returns An array of inpaint mask entity IDs
*/
getCompositeInpaintMaskEntityIds = (): string[] => {
const validSortedIds = [];
const sortedIds = this.manager.stateApi.getInpaintMasksState().entities.map(({ id }) => id);
for (const id of sortedIds) {
const adapter = this.manager.adapters.inpaintMasks.get(id);
if (!adapter) {
this.log.warn({ id }, 'Inpaint mask adapter not found');
continue;
}
if (adapter.state.isEnabled && adapter.state.objects.length > 0) {
validSortedIds.push(adapter.id);
}
}
return validSortedIds;
};
/**
* Gets a hash of the composite inpaint mask, which includes the state of all inpaint masks that are included in the
* composite plus arbitrary extra data that should contribute to the hash (e.g. a rect).
* @param extra Any extra data to include in the hash
* @returns A hash for the composite inpaint mask
*/
getCompositeInpaintMaskHash = (extra: SerializableObject): string => {
const adapterHashes: SerializableObject[] = [];
for (const id of this.getCompositeInpaintMaskEntityIds()) {
const adapter = this.manager.adapters.inpaintMasks.get(id);
if (!adapter) {
this.log.warn({ id }, 'Inpaint mask adapter not found');
continue;
}
adapterHashes.push(adapter.getHashableState());
}
const data: SerializableObject = {
extra,
adapterHashes,
};
return stableHash(data);
};
/**
* Gets a canvas element for the composite inpaint mask. Only the region defined by the rect is included in the canvas.
*
* If the hash of the composite inpaint mask is found in the cache, the cached canvas is returned.
*
* @param rect The region to include in the canvas
* @returns A canvas element with the composite inpaint mask drawn on it
*/
getCompositeInpaintMaskCanvas = (rect: Rect): HTMLCanvasElement => {
const hash = this.getCompositeInpaintMaskHash({ rect });
const cachedCanvas = this.manager.cache.canvasElementCache.get(hash);
if (cachedCanvas) {
this.log.trace({ rect }, 'Using cached composite inpaint mask canvas');
return cachedCanvas;
}
this.log.trace({ rect }, 'Building composite inpaint mask canvas');
this.$isCompositing.set(true);
const canvas = document.createElement('canvas');
canvas.width = rect.width;
canvas.height = rect.height;
const ctx = canvas.getContext('2d');
assert(ctx !== null);
ctx.imageSmoothingEnabled = false;
for (const id of this.getCompositeInpaintMaskEntityIds()) {
const adapter = this.manager.adapters.inpaintMasks.get(id);
if (!adapter) {
this.log.warn({ id }, 'Inpaint mask adapter not found');
continue;
}
this.log.trace({ id }, 'Drawing inpaint mask to composite canvas');
const adapterCanvas = adapter.getCanvas(rect);
ctx.drawImage(adapterCanvas, 0, 0);
}
this.manager.cache.canvasElementCache.set(hash, canvas);
this.$isCompositing.set(false);
return canvas;
};
/**
* Rasterizes the composite inpaint mask and uploads it to the server.
*
* If the hash of the composite inpaint mask is found in the cache, the cached image DTO is returned.
*
* @param rect The region to include in the rasterized image
* @param saveToGallery Whether to save the image to the gallery or just return the uploaded image DTO
* @returns A promise that resolves to the uploaded image DTO
*/
rasterizeAndUploadCompositeInpaintMask = async (rect: Rect, saveToGallery: boolean) => {
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect');
const canvas = this.getCompositeInpaintMaskCanvas(rect);
this.$isProcessing.set(true);
const blobResult = await withResultAsync(() => canvasToBlob(canvas));
this.$isProcessing.set(false);
if (blobResult.isErr()) {
throw blobResult.error;
}
const blob = blobResult.value;
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite inpaint mask canvas');
}
this.$isUploading.set(true);
const uploadResult = await withResultAsync(() =>
uploadImage({
blob,
fileName: 'composite-inpaint-mask.png',
image_category: 'general',
is_intermediate: !saveToGallery,
board_id: saveToGallery ? selectAutoAddBoardId(this.manager.store.getState()) : undefined,
})
);
this.$isUploading.set(false);
if (uploadResult.isErr()) {
throw uploadResult.error;
}
const imageDTO = uploadResult.value;
return imageDTO;
};
/**
* Gets the image DTO for the composite inpaint mask.
*
* If the image is found in the cache, the cached image DTO is returned.
*
* @param rect The region to include in the image
* @returns A promise that resolves to the image DTO
*/
getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
const hash = this.getCompositeInpaintMaskHash({ rect });
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
if (cachedImageName) {
imageDTO = await getImageDTOSafe(cachedImageName);
if (imageDTO) {
this.log.trace({ rect, cachedImageName, imageDTO }, 'Using cached composite inpaint mask image');
return imageDTO;
}
}
imageDTO = await this.rasterizeAndUploadCompositeInpaintMask(rect, false);
imageDTO = uploadResult.value;
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
@@ -407,23 +253,29 @@ export class CanvasCompositorModule extends CanvasModuleBase {
getGenerationMode(): GenerationMode {
const { rect } = this.manager.stateApi.getBbox();
const compositeInpaintMaskHash = this.getCompositeInpaintMaskHash({ rect });
const compositeRasterLayerHash = this.getCompositeRasterLayerHash({ rect });
const rasterAdapters = this.manager.compositor.getVisibleAdaptersOfType('raster_layer');
const compositeRasterLayerHash = this.getCompositeHash(rasterAdapters, { rect });
const inpaintMaskAdapters = this.manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
const compositeInpaintMaskHash = this.getCompositeHash(inpaintMaskAdapters, { rect });
const hash = stableHash({ rect, compositeInpaintMaskHash, compositeRasterLayerHash });
const cachedGenerationMode = this.manager.cache.generationModeCache.get(hash);
if (cachedGenerationMode) {
this.log.trace({ rect, cachedGenerationMode }, 'Using cached generation mode');
this.log.debug({ rect, cachedGenerationMode }, 'Using cached generation mode');
return cachedGenerationMode;
}
const compositeInpaintMaskCanvas = this.getCompositeInpaintMaskCanvas(rect);
this.log.debug({ rect }, 'Calculating generation mode');
const compositeInpaintMaskCanvas = this.getCompositeCanvas(inpaintMaskAdapters, rect);
this.$isProcessing.set(true);
const compositeInpaintMaskImageData = canvasToImageData(compositeInpaintMaskCanvas);
const compositeInpaintMaskTransparency = getImageDataTransparency(compositeInpaintMaskImageData);
this.$isProcessing.set(false);
const compositeRasterLayerCanvas = this.getCompositeRasterLayerCanvas(rect);
const compositeRasterLayerCanvas = this.getCompositeCanvas(rasterAdapters, rect);
this.$isProcessing.set(true);
const compositeRasterLayerImageData = canvasToImageData(compositeRasterLayerCanvas);
const compositeRasterLayerTransparency = getImageDataTransparency(compositeRasterLayerImageData);

View File

@@ -187,6 +187,18 @@ export class CanvasManager extends CanvasModuleBase {
}
};
getAdapters = (entityIdentifiers: CanvasEntityIdentifier[]): CanvasEntityAdapter[] => {
const adapters: CanvasEntityAdapter[] = [];
for (const entityIdentifier of entityIdentifiers) {
const adapter = this.getAdapter(entityIdentifier);
if (!adapter) {
continue;
}
adapters.push(adapter);
}
return adapters;
};
getAllAdapters = (): CanvasEntityAdapter[] => {
return [
...this.adapters.rasterLayers.values(),

View File

@@ -332,6 +332,7 @@ const zCanvasRenderableEntityState = z.discriminatedUnion('type', [
zCanvasInpaintMaskState,
]);
export type CanvasRenderableEntityState = z.infer<typeof zCanvasRenderableEntityState>;
export type CanvasRenderableEntityType = CanvasRenderableEntityState['type'];
const zCanvasEntityType = z.union([
zCanvasRasterLayerState.shape.type,