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