| | import { withResultAsync } from 'common/util/result'; |
| | import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule'; |
| | import type { CanvasEntityAdapter, CanvasEntityAdapterFromType } from 'features/controlLayers/konva/CanvasEntity/types'; |
| | import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; |
| | import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; |
| | import type { Transparency } from 'features/controlLayers/konva/util'; |
| | import { |
| | canvasToBlob, |
| | canvasToImageData, |
| | getImageDataTransparency, |
| | getPrefixedId, |
| | getRectUnion, |
| | mapId, |
| | previewBlob, |
| | } from 'features/controlLayers/konva/util'; |
| | import { |
| | selectActiveControlLayerEntities, |
| | selectActiveInpaintMaskEntities, |
| | selectActiveRasterLayerEntities, |
| | selectActiveRegionalGuidanceEntities, |
| | } from 'features/controlLayers/store/selectors'; |
| | import type { |
| | CanvasRenderableEntityIdentifier, |
| | CanvasRenderableEntityState, |
| | CanvasRenderableEntityType, |
| | GenerationMode, |
| | Rect, |
| | } from 'features/controlLayers/store/types'; |
| | import { getEntityIdentifier } from 'features/controlLayers/store/types'; |
| | import { imageDTOToImageObject } from 'features/controlLayers/store/util'; |
| | import { toast } from 'features/toast/toast'; |
| | import { t } from 'i18next'; |
| | import { atom, computed } from 'nanostores'; |
| | import type { Logger } from 'roarr'; |
| | import { serializeError } from 'serialize-error'; |
| | import { getImageDTOSafe, uploadImage } from 'services/api/endpoints/images'; |
| | import type { ImageDTO, UploadImageArg } from 'services/api/types'; |
| | import stableHash from 'stable-hash'; |
| | import type { Equals } from 'tsafe'; |
| | import { assert } from 'tsafe'; |
| | import type { JsonObject, SetOptional } from 'type-fest'; |
| |
|
| | type CompositingOptions = { |
| | |
| | |
| | |
| | |
| | globalCompositeOperation?: GlobalCompositeOperation; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export class CanvasCompositorModule extends CanvasModuleBase { |
| | readonly type = 'compositor'; |
| | readonly id: string; |
| | readonly path: string[]; |
| | readonly log: Logger; |
| | readonly parent: CanvasManager; |
| | readonly manager: CanvasManager; |
| |
|
| | $isCompositing = atom(false); |
| | $isProcessing = atom(false); |
| | $isUploading = atom(false); |
| | $isBusy = computed( |
| | [this.$isCompositing, this.$isProcessing, this.$isUploading], |
| | (isCompositing, isProcessing, isUploading) => { |
| | return isCompositing || isProcessing || isUploading; |
| | } |
| | ); |
| |
|
| | constructor(manager: CanvasManager) { |
| | super(); |
| | this.id = getPrefixedId('canvas_compositor'); |
| | this.parent = manager; |
| | this.manager = manager; |
| | this.path = this.manager.buildPath(this); |
| | this.log = this.manager.buildLogger(this); |
| | this.log.debug('Creating compositor module'); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getVisibleRectOfType = (type?: CanvasRenderableEntityType): Rect => { |
| | const rects = []; |
| |
|
| | for (const adapter of this.manager.getAllAdapters()) { |
| | if (!adapter.state.isEnabled) { |
| | continue; |
| | } |
| | if (type && adapter.state.type !== type) { |
| | continue; |
| | } |
| | if (adapter.renderer.hasObjects()) { |
| | rects.push(adapter.transformer.getRelativeRect()); |
| | } |
| | } |
| |
|
| | return getRectUnion(...rects); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getRectOfAdapters = (adapters: CanvasEntityAdapter[]): Rect => { |
| | const rects = []; |
| |
|
| | for (const adapter of adapters) { |
| | if (adapter.renderer.hasObjects()) { |
| | rects.push(adapter.transformer.getRelativeRect()); |
| | } |
| | } |
| |
|
| | return getRectUnion(...rects); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getVisibleAdaptersOfType = <T extends CanvasRenderableEntityType>(type: T): CanvasEntityAdapterFromType<T>[] => { |
| | let entities: CanvasRenderableEntityState[]; |
| |
|
| | 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}`); |
| | } |
| |
|
| | const adapters: CanvasEntityAdapter[] = entities |
| | |
| | .map((entity) => getEntityIdentifier(entity)) |
| | |
| | .map(this.manager.getAdapter) |
| | |
| | .filter((adapter) => !!adapter) |
| | |
| | .filter((adapter) => !adapter.$isDisabled.get() && adapter.renderer.hasObjects()); |
| |
|
| | return adapters as CanvasEntityAdapterFromType<T>[]; |
| | }; |
| |
|
| | getCompositeHash = (adapters: CanvasEntityAdapter[], extra: JsonObject): string => { |
| | const adapterHashes: JsonObject[] = []; |
| |
|
| | for (const adapter of adapters) { |
| | adapterHashes.push(adapter.getHashableState()); |
| | } |
| |
|
| | const data: JsonObject = { |
| | extra, |
| | adapterHashes, |
| | }; |
| |
|
| | return stableHash(data); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getCompositeCanvas = ( |
| | adapters: CanvasEntityAdapter[], |
| | rect: Rect, |
| | compositingOptions?: CompositingOptions |
| | ): 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.debug({ entityIdentifiers, rect }, 'Using cached composite canvas'); |
| | return cachedCanvas; |
| | } |
| |
|
| | this.log.debug({ entityIdentifiers, rect }, 'Building composite 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, 'Canvas 2D context is null'); |
| |
|
| | ctx.imageSmoothingEnabled = false; |
| |
|
| | if (compositingOptions?.globalCompositeOperation) { |
| | ctx.globalCompositeOperation = compositingOptions.globalCompositeOperation; |
| | } |
| |
|
| | 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); |
| | } |
| | this.manager.cache.canvasElementCache.set(hash, canvas); |
| | this.$isCompositing.set(false); |
| | return canvas; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getCompositeImageDTO = async ( |
| | adapters: CanvasEntityAdapter[], |
| | rect: Rect, |
| | uploadOptions: SetOptional<Omit<UploadImageArg, 'file'>, 'image_category'>, |
| | compositingOptions?: CompositingOptions, |
| | forceUpload?: boolean |
| | ): Promise<ImageDTO> => { |
| | assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect'); |
| |
|
| | const hash = this.getCompositeHash(adapters, { rect }); |
| | const cachedImageName = forceUpload ? undefined : 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, compositingOptions); |
| |
|
| | 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'); |
| | } |
| |
|
| | this.$isUploading.set(true); |
| | const uploadResult = await withResultAsync(() => |
| | uploadImage({ |
| | file: new File([blob], 'canvas-composite.png', { type: 'image/png' }), |
| | image_category: 'general', |
| | ...uploadOptions, |
| | }) |
| | ); |
| | this.$isUploading.set(false); |
| | if (uploadResult.isErr()) { |
| | throw uploadResult.error; |
| | } |
| | imageDTO = uploadResult.value; |
| | this.manager.cache.imageNameCache.set(hash, imageDTO.image_name); |
| | return imageDTO; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | mergeByEntityIdentifiers = async <T extends CanvasRenderableEntityIdentifier>( |
| | entityIdentifiers: T[], |
| | deleteMergedEntities: boolean |
| | ): Promise<ImageDTO | null> => { |
| | toast({ id: 'MERGE_LAYERS_TOAST', title: t('controlLayers.mergingLayers'), withCount: false }); |
| | if (entityIdentifiers.length <= 1) { |
| | this.log.warn({ entityIdentifiers }, 'Cannot merge less than 2 entities'); |
| | return null; |
| | } |
| | const type = entityIdentifiers[0]?.type; |
| | assert(type, 'Cannot merge entities with no type (this should never happen)'); |
| |
|
| | const adapters = this.manager.getAdapters(entityIdentifiers); |
| | assert(adapters.length === entityIdentifiers.length, 'Failed to get all adapters for entity identifiers'); |
| |
|
| | const rect = this.getRectOfAdapters(adapters); |
| |
|
| | const compositingOptions: CompositingOptions = { |
| | globalCompositeOperation: type === 'control_layer' ? 'lighter' : undefined, |
| | }; |
| |
|
| | const result = await withResultAsync(() => |
| | this.getCompositeImageDTO(adapters, rect, { is_intermediate: true }, compositingOptions) |
| | ); |
| |
|
| | if (result.isErr()) { |
| | this.log.error({ error: serializeError(result.error) }, 'Failed to merge selected entities'); |
| | toast({ |
| | id: 'MERGE_LAYERS_TOAST', |
| | title: t('controlLayers.mergeVisibleError'), |
| | status: 'error', |
| | withCount: false, |
| | }); |
| | return null; |
| | } |
| |
|
| | |
| | |
| | const addEntityArg = { |
| | isSelected: true, |
| | overrides: { |
| | objects: [imageDTOToImageObject(result.value)], |
| | position: { x: Math.floor(rect.x), y: Math.floor(rect.y) }, |
| | }, |
| | mergedEntitiesToDelete: deleteMergedEntities ? entityIdentifiers.map(mapId) : [], |
| | }; |
| |
|
| | switch (type) { |
| | case 'raster_layer': |
| | this.manager.stateApi.addRasterLayer(addEntityArg); |
| | break; |
| | case 'inpaint_mask': |
| | this.manager.stateApi.addInpaintMask(addEntityArg); |
| | break; |
| | case 'regional_guidance': |
| | this.manager.stateApi.addRegionalGuidance(addEntityArg); |
| | break; |
| | case 'control_layer': |
| | this.manager.stateApi.addControlLayer(addEntityArg); |
| | break; |
| | default: |
| | assert<Equals<typeof type, never>>(false, 'Unsupported type for merge'); |
| | } |
| |
|
| | toast({ id: 'MERGE_LAYERS_TOAST', title: t('controlLayers.mergeVisibleOk'), status: 'success', withCount: false }); |
| |
|
| | return result.value; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | mergeVisibleOfType = (type: CanvasRenderableEntityType): Promise<ImageDTO | null> => { |
| | let entities: CanvasRenderableEntityState[]; |
| |
|
| | switch (type) { |
| | case 'raster_layer': |
| | entities = this.manager.stateApi.runSelector(selectActiveRasterLayerEntities); |
| | break; |
| | case 'inpaint_mask': |
| | entities = this.manager.stateApi.runSelector(selectActiveInpaintMaskEntities); |
| | break; |
| | case 'regional_guidance': |
| | entities = this.manager.stateApi.runSelector(selectActiveRegionalGuidanceEntities); |
| | break; |
| | case 'control_layer': |
| | entities = this.manager.stateApi.runSelector(selectActiveControlLayerEntities); |
| | break; |
| | default: |
| | assert<Equals<typeof type, never>>(false, 'Unsupported type for merge'); |
| | } |
| |
|
| | const entityIdentifiers = entities.map(getEntityIdentifier); |
| |
|
| | return this.mergeByEntityIdentifiers(entityIdentifiers, false); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getTransparency = (adapters: CanvasEntityAdapter[], rect: Rect, hash: string): Promise<Transparency> => { |
| | const entityIdentifiers = adapters.map((adapter) => adapter.entityIdentifier); |
| | const logCtx = { entityIdentifiers, rect }; |
| | return CanvasCacheModule.getWithFallback({ |
| | cache: this.manager.cache.transparencyCalculationCache, |
| | key: hash, |
| | getValue: async () => { |
| | const compositeInpaintMaskCanvas = this.getCompositeCanvas(adapters, rect); |
| |
|
| | const compositeInpaintMaskImageData = await CanvasCacheModule.getWithFallback({ |
| | cache: this.manager.cache.imageDataCache, |
| | key: hash, |
| | getValue: () => Promise.resolve(canvasToImageData(compositeInpaintMaskCanvas)), |
| | onHit: () => this.log.trace(logCtx, 'Using cached image data'), |
| | onMiss: () => this.log.trace(logCtx, 'Calculating image data'), |
| | }); |
| |
|
| | return getImageDataTransparency(compositeInpaintMaskImageData); |
| | }, |
| | onHit: () => this.log.trace(logCtx, 'Using cached transparency'), |
| | onMiss: () => this.log.trace(logCtx, 'Calculating transparency'), |
| | }); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getGenerationMode = async (): Promise<GenerationMode> => { |
| | const { rect } = this.manager.stateApi.getBbox(); |
| |
|
| | const rasterLayerAdapters = this.manager.compositor.getVisibleAdaptersOfType('raster_layer'); |
| | const compositeRasterLayerHash = this.getCompositeHash(rasterLayerAdapters, { 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.debug({ rect, cachedGenerationMode }, 'Using cached generation mode'); |
| | return cachedGenerationMode; |
| | } |
| |
|
| | this.log.debug({ rect }, 'Calculating generation mode'); |
| |
|
| | this.$isProcessing.set(true); |
| | const compositeRasterLayerTransparency = await this.getTransparency( |
| | rasterLayerAdapters, |
| | rect, |
| | compositeRasterLayerHash |
| | ); |
| |
|
| | const compositeInpaintMaskTransparency = await this.getTransparency( |
| | inpaintMaskAdapters, |
| | rect, |
| | compositeInpaintMaskHash |
| | ); |
| | this.$isProcessing.set(false); |
| |
|
| | let generationMode: GenerationMode; |
| | if (compositeRasterLayerTransparency === 'FULLY_TRANSPARENT') { |
| | |
| | generationMode = 'txt2img'; |
| | } else if (compositeRasterLayerTransparency === 'PARTIALLY_TRANSPARENT') { |
| | |
| | generationMode = 'outpaint'; |
| | } else if (compositeInpaintMaskTransparency === 'FULLY_TRANSPARENT') { |
| | |
| | |
| | generationMode = 'img2img'; |
| | } else { |
| | |
| | generationMode = 'inpaint'; |
| | } |
| |
|
| | this.manager.cache.generationModeCache.set(hash, generationMode); |
| | return generationMode; |
| | }; |
| |
|
| | repr = () => { |
| | return { |
| | id: this.id, |
| | type: this.type, |
| | path: this.path, |
| | $isCompositing: this.$isCompositing.get(), |
| | $isProcessing: this.$isProcessing.get(), |
| | $isUploading: this.$isUploading.get(), |
| | $isBusy: this.$isBusy.get(), |
| | }; |
| | }; |
| | } |
| |
|