From 301da97670a322dbe96cce5ac61eefd0a3e4fbfe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:12:31 +1000 Subject: [PATCH] feat(ui): add CanvasModuleBase class to standardize canvas APIs I did this ages ago but undid it for some reason, not sure why. Caught a few issues related to subscriptions. --- .../components/StageComponent.tsx | 4 +- .../konva/CanvasBackgroundModule.ts | 37 ++++++-- .../controlLayers/konva/CanvasBboxModule.ts | 35 ++++--- .../controlLayers/konva/CanvasBrushLine.ts | 22 +++-- .../controlLayers/konva/CanvasCacheModule.ts | 27 +++++- .../konva/CanvasCompositorModule.ts | 24 ++++- .../controlLayers/konva/CanvasEraserLine.ts | 22 +++-- .../controlLayers/konva/CanvasFilterModule.ts | 18 ++-- .../controlLayers/konva/CanvasImage.ts | 14 ++- .../controlLayers/konva/CanvasLayerAdapter.ts | 16 +++- .../controlLayers/konva/CanvasManager.ts | 94 ++++++++++++------- .../controlLayers/konva/CanvasMaskAdapter.ts | 17 +++- .../controlLayers/konva/CanvasModuleBase.ts | 20 ++++ .../konva/CanvasObjectRenderer.ts | 19 ++-- .../konva/CanvasPreviewModule.ts | 40 +++++++- .../konva/CanvasProgressImageModule.ts | 27 ++++-- .../controlLayers/konva/CanvasRect.ts | 27 +++--- .../konva/CanvasRenderingModule.ts | 22 ++++- .../controlLayers/konva/CanvasStageModule.ts | 35 +++++-- .../konva/CanvasStagingAreaModule.ts | 13 ++- .../konva/CanvasStateApiModule.ts | 40 +++++++- .../controlLayers/konva/CanvasToolModule.ts | 37 +++++--- .../controlLayers/konva/CanvasTransformer.ts | 16 ++-- .../controlLayers/konva/CanvasWorkerModule.ts | 29 +++++- 24 files changed, 469 insertions(+), 186 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 1502491e73..81af6c00d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -38,8 +38,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, } const manager = new CanvasManager(stage, container, store, socket); - const cleanup = manager.initialize(); - return cleanup; + manager.initialize(); + return manager.destroy; }, [asPreview, container, socket, stage, store]); useLayoutEffect(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index 722d5a39e5..b8f0bd6b2d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -1,29 +1,35 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasBackgroundModule { - readonly type = 'background_grid'; +export class CanvasBackgroundModule extends CanvasModuleBase { + readonly type = 'background'; static GRID_LINE_COLOR_COARSE = getArbitraryBaseColor(27); static GRID_LINE_COLOR_FINE = getArbitraryBaseColor(18); id: string; + path: string[]; manager: CanvasManager; + subscriptions = new Set<() => void>(); + log: Logger; konva: { layer: Konva.Layer; }; - /** - * A set of subscriptions that should be cleaned up when the transformer is destroyed. - */ - subscriptions: Set<() => void> = new Set(); - constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId(this.type); this.manager = manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating background module'); + this.konva = { layer: new Konva.Layer({ name: `${this.type}:layer`, listening: false }) }; this.subscriptions.add( @@ -116,9 +122,8 @@ export class CanvasBackgroundModule { } destroy = () => { - for (const cleanup of this.subscriptions) { - cleanup(); - } + this.log.trace('Destroying background module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.layer.destroy(); }; @@ -145,4 +150,16 @@ export class CanvasBackgroundModule { } return 256; }; + + repr = () => { + return { + id: this.id, + path: this.path, + type: this.type, + }; + }; + + getLoggingContext = () => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index 4a2692752d..a84ba5c713 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Rect } from 'features/controlLayers/store/types'; @@ -22,20 +23,17 @@ const ALL_ANCHORS: string[] = [ const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const NO_ANCHORS: string[] = []; -export class CanvasBboxModule { - readonly type = 'generation_bbox'; +export class CanvasBboxModule extends CanvasModuleBase { + readonly type = 'bbox'; id: string; path: string[]; - parent: CanvasPreviewModule; manager: CanvasManager; log: Logger; - - /** - * A set of subscriptions that should be cleaned up when the transformer is destroyed. - */ subscriptions: Set<() => void> = new Set(); + parent: CanvasPreviewModule; + konva: { group: Konva.Group; rect: Konva.Rect; @@ -43,13 +41,14 @@ export class CanvasBboxModule { }; constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; - this.path = this.manager.path.concat(this.id); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating bbox'); + this.log.debug('Creating bbox module'); // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. @@ -238,7 +237,7 @@ export class CanvasBboxModule { } render = () => { - this.log.trace('Rendering generation bbox'); + this.log.trace('Rendering bbox module'); const bbox = this.manager.stateApi.getBbox(); const tool = this.manager.stateApi.$tool.get(); @@ -261,15 +260,21 @@ export class CanvasBboxModule { }); }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + destroy = () => { - this.log.trace('Destroying generation bbox'); - for (const unsubscribe of this.subscriptions) { - unsubscribe(); - } + this.log.trace('Destroying bbox module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); }; getLoggingContext = (): SerializableObject => { - return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 07641dfca0..3dac0b77cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,13 +1,13 @@ -import type { SerializableObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasBrushLineRenderer { +export class CanvasBrushLineRenderer extends CanvasModuleBase { readonly type = 'brush_line_renderer'; id: string; @@ -15,6 +15,7 @@ export class CanvasBrushLineRenderer { parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasBrushLineState; konva: { @@ -23,6 +24,7 @@ export class CanvasBrushLineRenderer { }; constructor(state: CanvasBrushLineState, parent: CanvasObjectRenderer) { + super(); const { id, clip } = state; this.id = id; this.parent = parent; @@ -30,7 +32,7 @@ export class CanvasBrushLineRenderer { this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating brush line'); + this.log.debug({ state }, 'Creating brush line renderer module'); this.konva = { group: new Konva.Group({ @@ -69,26 +71,28 @@ export class CanvasBrushLineRenderer { return false; } - destroy() { - this.log.trace('Destroying brush line'); + destroy = () => { + this.log.debug('Destroying brush line renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); - } + }; setVisibility(isVisible: boolean): void { this.log.trace({ isVisible }, 'Setting brush line visibility'); this.konva.group.visible(isVisible); } - repr() { + repr = () => { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, state: deepClone(this.state), }; - } + }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts index c81bde7fd4..90feeed82b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts @@ -1,26 +1,31 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { GenerationMode } from 'features/controlLayers/store/types'; import { LRUCache } from 'lru-cache'; import type { Logger } from 'roarr'; -export class CanvasCacheModule { +export class CanvasCacheModule extends CanvasModuleBase { + readonly type = 'cache'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); imageNameCache = new LRUCache({ max: 100 }); canvasElementCache = new LRUCache({ max: 32 }); generationModeCache = new LRUCache({ max: 100 }); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('cache'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas cache'); + + this.log.debug('Creating cache module'); } clearAll = () => { @@ -29,7 +34,21 @@ export class CanvasCacheModule { this.generationModeCache.clear(); }; - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + path: this.path, + type: this.type, + }; + }; + + destroy = () => { + this.log.debug('Destroying cache module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.clearAll(); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index f2f20b0937..2a93e79b14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -1,5 +1,6 @@ import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { canvasToBlob, canvasToImageData, @@ -14,18 +15,22 @@ import type { ImageDTO } from 'services/api/types'; import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasCompositorModule { +export class CanvasCompositorModule extends CanvasModuleBase { + readonly type = 'compositor'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('canvas_compositor'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas compositor'); + this.log.debug('Creating compositor module'); } getCompositeRasterLayerEntityIds = (): string[] => { @@ -237,7 +242,20 @@ export class CanvasCompositorModule { return generationMode; } - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.trace('Destroying compositor module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index b0b74fa51d..29b06ab9b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,12 +1,12 @@ -import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasEraserLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasEraserLineRenderer { +export class CanvasEraserLineRenderer extends CanvasModuleBase { readonly type = 'eraser_line_renderer'; id: string; @@ -14,6 +14,7 @@ export class CanvasEraserLineRenderer { parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasEraserLineState; konva: { @@ -22,6 +23,7 @@ export class CanvasEraserLineRenderer { }; constructor(state: CanvasEraserLineState, parent: CanvasObjectRenderer) { + super(); const { id, clip } = state; this.id = id; this.parent = parent; @@ -29,7 +31,7 @@ export class CanvasEraserLineRenderer { this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating eraser line'); + this.log.debug({ state }, 'Creating eraser line renderer module'); this.konva = { group: new Konva.Group({ @@ -68,26 +70,28 @@ export class CanvasEraserLineRenderer { return false; } - destroy() { - this.log.trace('Destroying eraser line'); + destroy = () => { + this.log.debug('Destroying eraser line renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); - } + }; setVisibility(isVisible: boolean): void { this.log.trace({ isVisible }, 'Setting brush line visibility'); this.konva.group.visible(isVisible); } - repr() { + repr = () => { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, state: deepClone(this.state), }; - } + }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index a32d752465..b0ce426832 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, CanvasImageState, FilterConfig } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, imageDTOToImageObject } from 'features/controlLayers/store/types'; @@ -10,15 +11,14 @@ import { getImageDTO } from 'services/api/endpoints/images'; import type { BatchConfig, ImageDTO, S } from 'services/api/types'; import { assert } from 'tsafe'; -const TYPE = 'entity_filter_preview'; - -export class CanvasFilterModule { - readonly type = TYPE; +export class CanvasFilterModule extends CanvasModuleBase { + readonly type = 'canvas_filter'; id: string; path: string[]; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); imageState: CanvasImageState | null = null; @@ -28,11 +28,13 @@ export class CanvasFilterModule { $config = atom(IMAGE_FILTERS.canny_image_processor.buildDefaults()); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId(this.type); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating filter'); + + this.log.debug('Creating filter module'); } initialize = (entityIdentifier: CanvasEntityIdentifier) => { @@ -167,17 +169,19 @@ export class CanvasFilterModule { }; destroy = () => { - this.log.trace('Destroying filter'); + this.log.trace('Destroying filter module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); }; repr = () => { return { id: this.id, type: this.type, + path: this.path, }; }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index f1b1a3abb9..6db8db5a7c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,8 +1,8 @@ import { Mutex } from 'async-mutex'; -import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule'; import { loadImage } from 'features/controlLayers/konva/util'; @@ -12,7 +12,7 @@ import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -export class CanvasImageRenderer { +export class CanvasImageRenderer extends CanvasModuleBase { readonly type = 'image_renderer'; id: string; @@ -20,6 +20,7 @@ export class CanvasImageRenderer { parent: CanvasObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasImageState; konva: { @@ -33,6 +34,7 @@ export class CanvasImageRenderer { mutex = new Mutex(); constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule) { + super(); const { id, image } = state; const { width, height } = image; this.id = id; @@ -41,7 +43,7 @@ export class CanvasImageRenderer { this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating image'); + this.log.debug({ state }, 'Creating image renderer module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -166,7 +168,8 @@ export class CanvasImageRenderer { }; destroy = () => { - this.log.trace('Destroying image'); + this.log.debug('Destroying image renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); }; @@ -179,6 +182,7 @@ export class CanvasImageRenderer { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, isLoading: this.isLoading, isError: this.isError, @@ -186,7 +190,7 @@ export class CanvasImageRenderer { }; }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 30f3c4f6be..47f803fbf9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getLastPointOfLine } from 'features/controlLayers/konva/util'; @@ -22,12 +23,13 @@ import type { Logger } from 'roarr'; import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasLayerAdapter { +export class CanvasLayerAdapter extends CanvasModuleBase { readonly type = 'layer_adapter'; id: string; path: string[]; manager: CanvasManager; + subscriptions = new Set<() => void>(); log: Logger; state: CanvasRasterLayerState | CanvasControlLayerState; @@ -41,11 +43,14 @@ export class CanvasLayerAdapter { isFirstRender: boolean = true; constructor(state: CanvasLayerAdapter['state'], manager: CanvasLayerAdapter['manager']) { + super(); this.id = state.id; this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug({ state }, 'Creating layer'); + + this.log.debug({ state }, 'Creating layer adapter module'); + this.state = state; this.konva = { @@ -71,10 +76,10 @@ export class CanvasLayerAdapter { }; destroy = (): void => { - this.log.debug('Destroying layer'); - // We need to call the destroy method on all children so they can do their own cleanup. - this.transformer.destroy(); + this.log.debug('Destroying layer adapter module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.renderer.destroy(); + this.transformer.destroy(); this.konva.layer.destroy(); }; @@ -144,6 +149,7 @@ export class CanvasLayerAdapter { return { id: this.id, type: this.type, + path: this.path, state: deepClone(this.state), transformer: this.transformer.repr(), renderer: this.renderer.repr(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b4131ddb05..b836eca30a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,6 +6,7 @@ import { SyncableMap } from 'common/util/SyncableMap/SyncableMap'; import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule'; import { CanvasCompositorModule } from 'features/controlLayers/konva/CanvasCompositorModule'; import { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasRenderingModule } from 'features/controlLayers/konva/CanvasRenderingModule'; import { CanvasStageModule } from 'features/controlLayers/konva/CanvasStageModule'; import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js'; @@ -21,17 +22,20 @@ import { CanvasPreviewModule } from './CanvasPreviewModule'; import { CanvasStateApiModule } from './CanvasStateApiModule'; export const $canvasManager = atom(null); -const TYPE = 'manager'; -export class CanvasManager { - readonly type = TYPE; +export class CanvasManager extends CanvasModuleBase { + readonly type = 'manager'; id: string; path: string[]; + manager: CanvasManager; + log: Logger; store: AppStore; socket: AppSocket; + subscriptions = new Set<() => void>(); + adapters = { rasterLayers: new SyncableMap(), controlLayers: new SyncableMap(), @@ -60,8 +64,21 @@ export class CanvasManager { _isDebugging: boolean = false; constructor(stage: Konva.Stage, container: HTMLDivElement, store: AppStore, socket: AppSocket) { + super(); this.id = getPrefixedId(this.type); this.path = [this.id]; + this.manager = this; + this.log = logger('canvas').child((message) => { + return { + ...message, + context: { + ...this.getLoggingContext(), + ...message.context, + }, + }; + }); + this.log.debug('Creating canvas manager module'); + this.store = store; this.socket = socket; @@ -80,16 +97,6 @@ export class CanvasManager { this.stage.addLayer(this.background.konva.layer); } - log = logger('canvas').child((message) => { - return { - ...message, - context: { - ...this.getLoggingContext(), - ...message.context, - }, - }; - }); - enableDebugging() { this._isDebugging = true; this.logDebugInfo(); @@ -100,7 +107,7 @@ export class CanvasManager { } initialize = () => { - this.log.debug('Initializing canvas manager'); + this.log.debug('Initializing canvas manager module'); // These atoms require the canvas manager to be set up before we can provide their initial values this.stateApi.$transformingEntity.set(null); @@ -109,26 +116,52 @@ export class CanvasManager { this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); - const cleanupStage = this.stage.initialize(); - const cleanupStore = this.store.subscribe(this.renderer.render); + this.subscriptions.add(this.store.subscribe(this.renderer.render)); + this.stage.initialize(); + }; - return () => { - this.log.debug('Cleaning up canvas manager'); - for (const adapter of this.adapters.getAll()) { - adapter.destroy(); - } - this.background.destroy(); - this.preview.destroy(); - cleanupStore(); - cleanupStage(); - }; + destroy = () => { + this.log.debug('Destroying canvas manager module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + for (const adapter of this.adapters.getAll()) { + adapter.destroy(); + } + this.stateApi.destroy(); + this.preview.destroy(); + this.background.destroy(); + this.filter.destroy(); + this.worker.destroy(); + this.renderer.destroy(); + this.compositor.destroy(); + this.stage.destroy(); + $canvasManager.set(null); }; setCanvasManager = () => { - this.log.debug('Setting canvas manager'); + this.log.debug('Setting canvas manager global'); $canvasManager.set(this); }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + rasterLayers: Array.from(this.adapters.rasterLayers.values()).map((adapter) => adapter.repr()), + controlLayers: Array.from(this.adapters.controlLayers.values()).map((adapter) => adapter.repr()), + inpaintMasks: Array.from(this.adapters.inpaintMasks.values()).map((adapter) => adapter.repr()), + regionMasks: Array.from(this.adapters.regionMasks.values()).map((adapter) => adapter.repr()), + stateApi: this.stateApi.repr(), + preview: this.preview.repr(), + background: this.background.repr(), + filter: this.filter.repr(), + worker: this.worker.repr(), + renderer: this.renderer.repr(), + compositor: this.compositor.repr(), + stage: this.stage.repr(), + }; + }; + getLoggingContext = (): SerializableObject => { return { path: this.path.join('.'), @@ -150,11 +183,6 @@ export class CanvasManager { logDebugInfo() { // eslint-disable-next-line no-console console.log('Canvas manager', this); - for (const adapter of this.adapters.getAll()) { - // eslint-disable-next-line no-console - console.log(adapter.id, adapter); - } + this.log.debug({ manager: this.repr() }, 'Canvas manager'); } - - getPrefixedId = getPrefixedId; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index ec2a50501e..a0aa213615 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getLastPointOfLine } from 'features/controlLayers/konva/util'; @@ -21,13 +22,14 @@ import { get, omit } from 'lodash-es'; import type { Logger } from 'roarr'; import stableHash from 'stable-hash'; -export class CanvasMaskAdapter { +export class CanvasMaskAdapter extends CanvasModuleBase { readonly type = 'mask_adapter'; id: string; path: string[]; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; @@ -41,11 +43,14 @@ export class CanvasMaskAdapter { }; constructor(state: CanvasMaskAdapter['state'], manager: CanvasMaskAdapter['manager']) { + super(); this.id = state.id; this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug({ state }, 'Creating mask'); + + this.log.debug({ state }, 'Creating mask adapter module'); + this.state = state; this.konva = { @@ -71,8 +76,8 @@ export class CanvasMaskAdapter { }; destroy = (): void => { - this.log.debug('Destroying mask'); - // We need to call the destroy method on all children so they can do their own cleanup. + this.log.debug('Destroying mask adapter module'); + this.transformer.destroy(); this.renderer.destroy(); this.konva.layer.destroy(); @@ -157,6 +162,7 @@ export class CanvasMaskAdapter { return { id: this.id, type: this.type, + path: this.path, state: deepClone(this.state), }; }; @@ -182,7 +188,8 @@ export class CanvasMaskAdapter { const canvas = this.renderer.getCanvas(rect, attrs); return canvas; }; - getLoggingContext = (): SerializableObject => { + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts new file mode 100644 index 0000000000..894c9622f7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts @@ -0,0 +1,20 @@ +import type { SerializableObject } from 'common/types'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { Logger } from 'roarr'; + +export abstract class CanvasModuleBase { + abstract id: string; + abstract type: string; + abstract path: string[]; + abstract manager: CanvasManager; + abstract log: Logger; + abstract subscriptions: Set<() => void>; + + abstract getLoggingContext: () => SerializableObject; + abstract destroy: () => void; + abstract repr: () => SerializableObject & { + id: string; + path: string[]; + type: string; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index c6c7194918..9a3b32cec8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -1,4 +1,3 @@ -import type { SerializableObject } from 'common/types'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; @@ -6,6 +5,7 @@ import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; @@ -55,8 +55,8 @@ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImage /** * Handles rendering of objects for a canvas entity. */ -export class CanvasObjectRenderer { - readonly type = 'object_renderer'; +export class CanvasObjectRenderer extends CanvasModuleBase { + readonly type = 'entity_object_renderer'; id: string; path: string[]; @@ -123,12 +123,13 @@ export class CanvasObjectRenderer { $canvasCache = atom<{ canvas: HTMLCanvasElement; rect: Rect } | null>(null); constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.path = this.parent.path.concat(this.id); this.manager = parent.manager; this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating object renderer'); + this.log.debug('Creating entity object renderer module'); this.konva = { objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }), @@ -597,11 +598,8 @@ export class CanvasObjectRenderer { * Destroys this renderer and all of its object renderers. */ destroy = () => { - this.log.trace('Destroying object renderer'); - for (const cleanup of this.subscriptions) { - this.log.trace('Cleaning up listener'); - cleanup(); - } + this.log.debug('Destroying entity object renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); for (const renderer of this.renderers.values()) { renderer.destroy(); } @@ -616,13 +614,14 @@ export class CanvasObjectRenderer { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, renderers: Array.from(this.renderers.values()).map((renderer) => renderer.repr()), buffer: this.bufferRenderer?.repr(), }; }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts index 6e2ef2bba7..0a3f47ee61 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts @@ -1,13 +1,22 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { Logger } from 'roarr'; import { CanvasBboxModule } from './CanvasBboxModule'; import { CanvasStagingAreaModule } from './CanvasStagingAreaModule'; import { CanvasToolModule } from './CanvasToolModule'; -export class CanvasPreviewModule { +export class CanvasPreviewModule extends CanvasModuleBase { + readonly type = 'preview'; + + id: string; + path: string[]; manager: CanvasManager; + log: Logger; + subscriptions = new Set<() => void>(); konva: { layer: Konva.Layer; @@ -19,7 +28,14 @@ export class CanvasPreviewModule { progressImage: CanvasProgressImageModule; constructor(manager: CanvasManager) { + super(); + this.id = getPrefixedId(this.type); this.manager = manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating preview module'); + this.konva = { layer: new Konva.Layer({ listening: false, imageSmoothingEnabled: false }), }; @@ -41,11 +57,25 @@ export class CanvasPreviewModule { return this.konva.layer; }; - destroy() { - // this.stagingArea.destroy(); // TODO(psyche): implement destroy + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.debug('Destroying preview module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.stagingArea.destroy(); this.progressImage.destroy(); - // this.bbox.destroy(); // TODO(psyche): implement destroy + this.bbox.destroy(); this.tool.destroy(); this.konva.layer.destroy(); - } + }; + + getLoggingContext = () => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts index b2b8a6977d..fb972aeded 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts @@ -1,13 +1,13 @@ import { Mutex } from 'async-mutex'; -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; import type { Logger } from 'roarr'; import type { S } from 'services/api/types'; -export class CanvasProgressImageModule { +export class CanvasProgressImageModule extends CanvasModuleBase { readonly type = 'progress_image'; id: string; @@ -35,13 +35,14 @@ export class CanvasProgressImageModule { mutex: Mutex = new Mutex(); constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = parent.manager; - this.path = this.manager.path.concat(this.id); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating progress image'); + this.log.debug('Creating progress image module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -105,15 +106,21 @@ export class CanvasProgressImageModule { } }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + destroy = () => { - this.log.trace('Destroying progress image'); - for (const unsubscribe of this.subscriptions) { - unsubscribe(); - } + this.log.debug('Destroying progress image module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); }; - getLoggingContext = (): SerializableObject => { - return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + getLoggingContext = () => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index fecaeb92b7..3cb0525e3f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,13 +1,13 @@ -import type { SerializableObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasRectState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasRectRenderer { +export class CanvasRectRenderer extends CanvasModuleBase { readonly type = 'rect_renderer'; id: string; @@ -15,6 +15,7 @@ export class CanvasRectRenderer { parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasRectState; konva: { @@ -24,13 +25,15 @@ export class CanvasRectRenderer { isFirstRender: boolean = false; constructor(state: CanvasRectState, parent: CanvasObjectRenderer) { + super(); const { id } = state; this.id = id; this.parent = parent; this.manager = parent.manager; this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating rect'); + + this.log.debug({ state }, 'Creating rect renderer module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -60,27 +63,29 @@ export class CanvasRectRenderer { return false; } - destroy() { - this.log.trace('Destroying rect'); - this.konva.group.destroy(); - } - setVisibility(isVisible: boolean): void { this.log.trace({ isVisible }, 'Setting rect visibility'); this.konva.group.visible(isVisible); } - repr() { + destroy = () => { + this.log.debug('Destroying rect renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.konva.group.destroy(); + }; + + repr = () => { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, isFirstRender: this.isFirstRender, state: deepClone(this.state), }; - } + }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index dee207c792..69236633bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -2,24 +2,29 @@ import type { SerializableObject } from 'common/types'; import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; -export class CanvasRenderingModule { +export class CanvasRenderingModule extends CanvasModuleBase { + readonly type = 'canvas_renderer'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); state: CanvasV2State | null = null; constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('canvas_renderer'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas renderer'); + this.log.debug('Creating canvas renderer module'); } render = async () => { @@ -263,4 +268,17 @@ export class CanvasRenderingModule { this.manager.preview.getLayer().zIndex(++zIndex); } }; + + repr = () => { + return { + id: this.id, + path: this.path, + type: this.type, + }; + }; + + destroy = () => { + this.log.debug('Destroying canvas renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index 995751de10..a23bfad9c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -1,5 +1,5 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CANVAS_SCALE_BY } from 'features/controlLayers/konva/constants'; import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util'; import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types'; @@ -8,7 +8,9 @@ import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; import type { Logger } from 'roarr'; -export class CanvasStageModule { +export class CanvasStageModule extends CanvasModuleBase { + readonly type = 'stage'; + static MIN_CANVAS_SCALE = 0.1; static MAX_CANVAS_SCALE = 20; @@ -19,12 +21,17 @@ export class CanvasStageModule { container: HTMLDivElement; log: Logger; + subscriptions = new Set<() => void>(); + constructor(stage: Konva.Stage, container: HTMLDivElement, manager: CanvasManager) { + super(); this.id = getPrefixedId('stage'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.debug('Creating stage module'); + this.container = container; this.konva = { stage }; } @@ -50,12 +57,10 @@ export class CanvasStageModule { this.fitLayersToStage(); const cleanupListeners = this.setEventListeners(); - return () => { - this.log.debug('Destroying stage'); + this.subscriptions.add(cleanupListeners); + this.subscriptions.add(() => { resizeObserver.disconnect(); - this.konva.stage.destroy(); - cleanupListeners(); - }; + }); }; fitStageToContainer = () => { @@ -276,7 +281,21 @@ export class CanvasStageModule { this.konva.stage.add(layer); }; - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.debug('Destroying stage module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.konva.stage.destroy(); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts index 225e21eeb7..202b367edb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts @@ -1,13 +1,14 @@ import type { SerializableObject } from 'common/types'; import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasStagingAreaModule { +export class CanvasStagingAreaModule extends CanvasModuleBase { readonly type = 'staging_area'; id: string; @@ -27,12 +28,14 @@ export class CanvasStagingAreaModule { subscriptions: Set<() => void> = new Set(); constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating staging area'); + + this.log.debug('Creating staging area module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }) }; this.image = null; @@ -85,12 +88,11 @@ export class CanvasStagingAreaModule { }; destroy = () => { + this.log.debug('Destroying staging area module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); if (this.image) { this.image.destroy(); } - for (const unsubscribe of this.subscriptions) { - unsubscribe(); - } for (const node of this.getNodes()) { node.destroy(); } @@ -100,6 +102,7 @@ export class CanvasStagingAreaModule { return { id: this.id, type: this.type, + path: this.path, selectedImage: this.selectedImage, }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 24d3c1bec1..60671a7f98 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -3,6 +3,8 @@ import type { AppStore } from 'app/store/store'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxChanged, brushWidthChanged, @@ -40,6 +42,7 @@ import type { import { RGBA_BLACK } from 'features/controlLayers/store/types'; import type { WritableAtom } from 'nanostores'; import { atom } from 'nanostores'; +import type { Logger } from 'roarr'; import { queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig } from 'services/api/types'; import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; @@ -70,13 +73,27 @@ type EntityStateAndAdapter = adapter: CanvasMaskAdapter; }; -export class CanvasStateApiModule { - store: AppStore; +export class CanvasStateApiModule extends CanvasModuleBase { + readonly type = 'state_api'; + + id: string; + path: string[]; manager: CanvasManager; + log: Logger; + subscriptions = new Set<() => void>(); + + store: AppStore; constructor(store: AppStore, manager: CanvasManager) { - this.store = store; + super(); + this.id = getPrefixedId(this.type); this.manager = manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating state api module'); + + this.store = store; } // Reminder - use arrow functions to avoid binding issues @@ -258,4 +275,21 @@ export class CanvasStateApiModule { height: 0, scale: 0, }); + + destroy = () => { + this.log.debug('Destroying state api module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + getLoggingContext = () => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 0bce3396ea..38bc0dd3ae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -1,6 +1,6 @@ -import type { SerializableObject } from 'common/types'; import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { BRUSH_BORDER_INNER_COLOR, @@ -31,8 +31,8 @@ import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Logger } from 'roarr'; -export class CanvasToolModule { - readonly type = 'tool_preview'; +export class CanvasToolModule extends CanvasModuleBase { + readonly type = 'tool'; static readonly COLOR_PICKER_RADIUS = 25; static readonly COLOR_PICKER_THICKNESS = 15; static readonly COLOR_PICKER_CROSSHAIR_SPACE = 5; @@ -84,11 +84,15 @@ export class CanvasToolModule { subscriptions: Set<() => void> = new Set(); constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; - this.path = this.manager.path.concat(this.id); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating tool module'); + this.konva = { stage: this.manager.stage.konva.stage, group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -240,13 +244,6 @@ export class CanvasToolModule { this.subscriptions.add(cleanupListeners); } - destroy = () => { - for (const cleanup of this.subscriptions) { - cleanup(); - } - this.konva.group.destroy(); - }; - setToolVisibility = (tool: Tool) => { this.konva.brush.group.visible(tool === 'brush'); this.konva.eraser.group.visible(tool === 'eraser'); @@ -882,7 +879,21 @@ export class CanvasToolModule { } }; - getLoggingContext = (): SerializableObject => { - return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.debug('Destroying tool module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.konva.group.destroy(); + }; + + getLoggingContext = () => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index f58105a25e..9c8e316851 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,7 +1,7 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -18,7 +18,7 @@ import type { Logger } from 'roarr'; * * It renders an outline when dragging and resizing the entity, with transform anchors for resizing and rotation. */ -export class CanvasTransformer { +export class CanvasTransformer extends CanvasModuleBase { readonly type = 'entity_transformer'; static RECT_CALC_DEBOUNCE_MS = 300; @@ -99,11 +99,13 @@ export class CanvasTransformer { }; constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = parent.manager; this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.debug('Creating entity transformer module'); this.konva = { outlineRect: new Konva.Rect({ @@ -721,6 +723,7 @@ export class CanvasTransformer { return { id: this.id, type: this.type, + path: this.path, mode: this.interactionMode, isTransformEnabled: this.isTransformEnabled, isDragEnabled: this.isDragEnabled, @@ -731,17 +734,14 @@ export class CanvasTransformer { * Destroys the transformer, cleaning up any subscriptions. */ destroy = () => { - this.log.trace('Destroying transformer'); - for (const cleanup of this.subscriptions) { - this.log.trace('Cleaning up listener'); - cleanup(); - } + this.log.debug('Destroying entity transformer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.outlineRect.destroy(); this.konva.transformer.destroy(); this.konva.proxyRect.destroy(); }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts index 9d61518be8..422b1f5ef4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts @@ -1,24 +1,29 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import type { Logger } from 'roarr'; -export class CanvasWorkerModule { +export class CanvasWorkerModule extends CanvasModuleBase { + readonly type = 'worker'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); worker: Worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); tasks: Map void }> = new Map(); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('worker'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas worker'); + + this.log.debug('Creating worker module'); this.worker.onmessage = (event: MessageEvent) => { const { type, data } = event.data; @@ -55,7 +60,23 @@ export class CanvasWorkerModule { this.worker.postMessage(task, [data.buffer]); } - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + tasks: Array.from(this.tasks.keys()), + }; + }; + + destroy = () => { + this.log.trace('Destroying worker module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.worker.terminate(); + this.tasks.clear(); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; }