| import { Mutex } from 'async-mutex'; |
| import { parseify } from 'common/util/serialize'; |
| import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; |
| import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; |
| import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; |
| import { selectShowProgressOnCanvas } from 'features/controlLayers/store/canvasSettingsSlice'; |
| import Konva from 'konva'; |
| import { atom } from 'nanostores'; |
| import type { Logger } from 'roarr'; |
| import { selectCanvasQueueCounts } from 'services/api/endpoints/queue'; |
| import type { S } from 'services/api/types'; |
| import type { SetNonNullable } from 'type-fest'; |
|
|
| type ProgressEventWithImage = SetNonNullable<S['InvocationProgressEvent'], 'image'>; |
| const isProgressEventWithImage = (val: S['InvocationProgressEvent']): val is ProgressEventWithImage => |
| Boolean(val.image); |
|
|
| export class CanvasProgressImageModule extends CanvasModuleBase { |
| readonly type = 'progress_image'; |
| readonly id: string; |
| readonly path: string[]; |
| readonly parent: CanvasManager; |
| readonly manager: CanvasManager; |
| readonly log: Logger; |
|
|
| konva: { |
| group: Konva.Group; |
| image: Konva.Image | null; |
| }; |
| $isLoading = atom<boolean>(false); |
| $isError = atom<boolean>(false); |
| imageElement: HTMLImageElement | null = null; |
|
|
| subscriptions = new Set<() => void>(); |
| $lastProgressEvent = atom<ProgressEventWithImage | null>(null); |
| $hasActiveGeneration = atom<boolean>(false); |
| mutex: Mutex = new Mutex(); |
|
|
| constructor(manager: CanvasManager) { |
| super(); |
| this.id = getPrefixedId(this.type); |
| this.parent = manager; |
| this.manager = manager; |
| this.path = this.manager.buildPath(this); |
| this.log = this.manager.buildLogger(this); |
|
|
| this.log.debug('Creating progress image module'); |
|
|
| this.konva = { |
| group: new Konva.Group({ name: `${this.type}:group`, listening: false }), |
| image: null, |
| }; |
|
|
| this.subscriptions.add(this.manager.stagingArea.$shouldShowStagedImage.listen(this.render)); |
| this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectShowProgressOnCanvas, this.render)); |
| this.subscriptions.add(this.setSocketEventListeners()); |
| this.subscriptions.add( |
| this.manager.stateApi.createStoreSubscription(selectCanvasQueueCounts, ({ data }) => { |
| if (data && (data.in_progress > 0 || data.pending > 0)) { |
| this.$hasActiveGeneration.set(true); |
| } else { |
| this.$hasActiveGeneration.set(false); |
| } |
| }) |
| ); |
| this.subscriptions.add(this.$lastProgressEvent.listen(this.render)); |
| } |
|
|
| setSocketEventListeners = (): (() => void) => { |
| const progressListener = (data: S['InvocationProgressEvent']) => { |
| if (data.destination !== 'canvas') { |
| return; |
| } |
| if (!isProgressEventWithImage(data)) { |
| return; |
| } |
| if (!this.$hasActiveGeneration.get()) { |
| return; |
| } |
| this.$lastProgressEvent.set(data); |
| }; |
|
|
| |
| const queueItemStatusChangedListener = (data: S['QueueItemStatusChangedEvent']) => { |
| if (data.destination !== 'canvas') { |
| return; |
| } |
| if (data.status === 'failed' || data.status === 'canceled') { |
| this.$lastProgressEvent.set(null); |
| this.$hasActiveGeneration.set(false); |
| } |
| }; |
|
|
| const clearProgress = () => { |
| this.$lastProgressEvent.set(null); |
| }; |
|
|
| this.manager.socket.on('invocation_progress', progressListener); |
| this.manager.socket.on('queue_item_status_changed', queueItemStatusChangedListener); |
| this.manager.socket.on('connect', clearProgress); |
| this.manager.socket.on('connect_error', clearProgress); |
| this.manager.socket.on('disconnect', clearProgress); |
|
|
| return () => { |
| this.manager.socket.off('invocation_progress', progressListener); |
| this.manager.socket.off('queue_item_status_changed', queueItemStatusChangedListener); |
| this.manager.socket.off('connect', clearProgress); |
| this.manager.socket.off('connect_error', clearProgress); |
| this.manager.socket.off('disconnect', clearProgress); |
| }; |
| }; |
|
|
| getNodes = () => { |
| return [this.konva.group]; |
| }; |
|
|
| render = async () => { |
| const release = await this.mutex.acquire(); |
|
|
| const event = this.$lastProgressEvent.get(); |
| const showProgressOnCanvas = this.manager.stateApi.runSelector(selectShowProgressOnCanvas); |
|
|
| if (!event || !showProgressOnCanvas) { |
| this.konva.group.visible(false); |
| this.konva.image?.destroy(); |
| this.konva.image = null; |
| this.imageElement = null; |
| this.$isLoading.set(false); |
| this.$isError.set(false); |
| release(); |
| return; |
| } |
|
|
| this.$isLoading.set(true); |
|
|
| const { x, y, width, height } = this.manager.stateApi.getBbox().rect; |
| try { |
| this.imageElement = await loadImage(event.image.dataURL); |
| if (this.konva.image) { |
| this.konva.image.setAttrs({ |
| image: this.imageElement, |
| x, |
| y, |
| width, |
| height, |
| }); |
| } else { |
| this.konva.image = new Konva.Image({ |
| name: `${this.type}:image`, |
| listening: false, |
| image: this.imageElement, |
| x, |
| y, |
| width, |
| height, |
| perfectDrawEnabled: false, |
| }); |
| this.konva.group.add(this.konva.image); |
| } |
| |
| this.konva.group.visible(this.manager.stagingArea.$shouldShowStagedImage.get()); |
| } catch { |
| this.$isError.set(true); |
| } finally { |
| this.$isLoading.set(false); |
| release(); |
| } |
| }; |
|
|
| destroy = () => { |
| this.log.debug('Destroying module'); |
| this.subscriptions.forEach((unsubscribe) => unsubscribe()); |
| this.subscriptions.clear(); |
| this.konva.group.destroy(); |
| }; |
|
|
| repr = () => { |
| return { |
| id: this.id, |
| type: this.type, |
| path: this.path, |
| $lastProgressEvent: parseify(this.$lastProgressEvent.get()), |
| $hasActiveGeneration: this.$hasActiveGeneration.get(), |
| $isError: this.$isError.get(), |
| $isLoading: this.$isLoading.get(), |
| }; |
| }; |
| } |
|
|