From e176e48fa3bc80461afc3cc3c0b7c795de9dfd0c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:38:58 +1000 Subject: [PATCH] feat(ui): streamlined state flow --- .../CanvasSettingsRecalculateRectsButton.tsx | 2 +- .../contexts/EntityAdapterContext.tsx | 18 +-- .../controlLayers/hooks/useEntityAdapter.ts | 10 +- .../konva/CanvasBackgroundModule.ts | 1 + .../controlLayers/konva/CanvasBboxModule.ts | 27 +++-- .../konva/CanvasControlLayerAdapter.ts | 81 ------------- .../konva/CanvasEntityAdapterBase.ts | 114 ++++++++++++++++-- .../konva/CanvasEntityAdapterControlLayer.ts | 69 +++++++++++ .../konva/CanvasEntityAdapterInpaintMask.ts | 75 ++++++++++++ .../konva/CanvasEntityAdapterRasterLayer.ts | 62 ++++++++++ .../CanvasEntityAdapterRegionalGuidance.ts | 75 ++++++++++++ .../konva/CanvasEntityObjectRenderer.ts | 8 +- .../konva/CanvasEntityRendererModule.ts | 104 +++------------- .../konva/CanvasEntityTransformer.ts | 3 +- .../controlLayers/konva/CanvasFilterModule.ts | 16 +-- .../konva/CanvasInpaintMaskAdapter.ts | 88 -------------- .../controlLayers/konva/CanvasManager.ts | 86 +++++++++---- .../controlLayers/konva/CanvasModuleBase.ts | 1 + .../konva/CanvasRasterLayerAdapter.ts | 80 ------------ .../konva/CanvasRegionalGuidanceAdapter.ts | 87 ------------- .../controlLayers/konva/CanvasStageModule.ts | 3 +- .../konva/CanvasStagingAreaModule.ts | 1 + .../konva/CanvasStateApiModule.ts | 16 +-- .../controlLayers/konva/CanvasToolModule.ts | 6 +- .../src/features/controlLayers/store/types.ts | 26 +++- 25 files changed, 557 insertions(+), 502 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterControlLayer.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterInpaintMask.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRasterLayer.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton.tsx index 53394a3db5..2eee581392 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton.tsx @@ -7,7 +7,7 @@ export const CanvasSettingsRecalculateRectsButton = memo(() => { const { t } = useTranslation(); const canvasManager = useCanvasManager(); const onClick = useCallback(() => { - for (const adapter of canvasManager.adapters.getAll()) { + for (const adapter of canvasManager.getAllAdapters()) { adapter.transformer.requestRectCalculation(); } }, [canvasManager]); diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx index 75e0563b46..ebbd74c092 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx @@ -1,15 +1,15 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; +import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntityAdapterControlLayer'; +import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntityAdapterInpaintMask'; +import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntityAdapterRasterLayer'; +import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance'; import type { PropsWithChildren } from 'react'; import { createContext, memo, useContext, useMemo, useSyncExternalStore } from 'react'; import { assert } from 'tsafe'; const EntityAdapterContext = createContext< - CanvasRasterLayerAdapter | CanvasControlLayerAdapter | CanvasInpaintMaskAdapter | CanvasRegionalGuidanceAdapter | null + CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer | CanvasEntityAdapterInpaintMask | CanvasEntityAdapterRegionalGuidance | null >(null); export const RasterLayerAdapterGate = memo(({ children }: PropsWithChildren) => { @@ -93,10 +93,10 @@ export const RegionalGuidanceAdapterGate = memo(({ children }: PropsWithChildren RegionalGuidanceAdapterGate.displayName = 'RegionalGuidanceAdapterGate'; export const useEntityAdapter = (): - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter => { + | CanvasEntityAdapterRasterLayer + | CanvasEntityAdapterControlLayer + | CanvasEntityAdapterInpaintMask + | CanvasEntityAdapterRegionalGuidance => { const adapter = useContext(EntityAdapterContext); assert(adapter, 'useEntityAdapter must be used within a CanvasRasterLayerAdapterGate'); return adapter; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts index 24c689fc9e..a12af75815 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts @@ -1,15 +1,15 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; +import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntityAdapterControlLayer'; +import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntityAdapterInpaintMask'; +import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntityAdapterRasterLayer'; +import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { assert } from 'tsafe'; export const useEntityAdapter = ( entityIdentifier: CanvasEntityIdentifier -): CanvasRasterLayerAdapter | CanvasControlLayerAdapter | CanvasInpaintMaskAdapter | CanvasRegionalGuidanceAdapter => { +): CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer | CanvasEntityAdapterInpaintMask | CanvasEntityAdapterRegionalGuidance => { const canvasManager = useCanvasManager(); const adapter = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index 7588aa1f9d..667c1a8882 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -196,6 +196,7 @@ export class CanvasBackgroundModule extends CanvasModuleBase { destroy = () => { this.log.trace('Destroying module'); this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); this.konva.layer.destroy(); }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index 40e99c4ec7..2e0d554e1a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -2,7 +2,7 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMult import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Coordinate, Rect } from 'features/controlLayers/store/types'; +import type { CanvasState, Coordinate, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; @@ -52,6 +52,8 @@ export class CanvasBboxModule extends CanvasModuleBase { */ $aspectRatioBuffer = atom(0); + state: CanvasState['bbox']; + constructor(manager: CanvasManager) { super(); this.id = getPrefixedId(this.type); @@ -63,8 +65,8 @@ export class CanvasBboxModule extends CanvasModuleBase { this.log.debug('Creating bbox module'); // Set the initial aspect ratio buffer per app state. - const bbox = this.manager.stateApi.getBbox(); - this.$aspectRatioBuffer.set(bbox.rect.width / bbox.rect.height); + this.state = this.manager.stateApi.getBbox(); + this.$aspectRatioBuffer.set(this.state.rect.width / this.state.rect.height); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: true }), @@ -75,10 +77,10 @@ export class CanvasBboxModule extends CanvasModuleBase { listening: false, strokeEnabled: false, draggable: true, - x: bbox.rect.x, - y: bbox.rect.y, - width: bbox.rect.width, - height: bbox.rect.height, + x: this.state.rect.x, + y: this.state.rect.y, + width: this.state.rect.width, + height: this.state.rect.height, }), transformer: new Konva.Transformer({ name: `${this.type}:transformer`, @@ -112,8 +114,18 @@ export class CanvasBboxModule extends CanvasModuleBase { // We will listen to the tool state to determine if the bbox should be visible or not. this.subscriptions.add(this.manager.tool.$tool.listen(this.render)); + this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync)); } + sync = () => { + const prevState = this.state; + this.state = this.manager.stateApi.getBbox(); + + if (this.state !== prevState) { + this.render(); + } + }; + /** * Renders the bbox. The bbox is only visible when the tool is set to 'bbox'. */ @@ -147,6 +159,7 @@ export class CanvasBboxModule extends CanvasModuleBase { destroy = () => { this.log.trace('Destroying module'); this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); this.konva.group.destroy(); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts deleted file mode 100644 index 8e2412a9af..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasControlLayerState, CanvasEntityIdentifier, Rect } from 'features/controlLayers/store/types'; -import type { GroupConfig } from 'konva/lib/Group'; -import { omit } from 'lodash-es'; -import { assert } from 'tsafe'; - -export class CanvasControlLayerAdapter extends CanvasEntityAdapterBase { - static TYPE = 'control_layer_adapter'; - private _state: CanvasControlLayerState | null = null; - - constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) { - super(entityIdentifier, manager, CanvasControlLayerAdapter.TYPE); - } - - get state(): CanvasControlLayerState { - if (this._state) { - return this._state; - } - const state = this.manager.stateApi.getControlLayersState().entities.find((layer) => layer.id === this.id); - assert(state, `State not found for ${this.id}`); - return state; - } - - set state(state: CanvasControlLayerState) { - const prevState = this._state; - this._state = state; - this.render(state, prevState); - } - - private render = async (state: CanvasControlLayerState, prevState: CanvasControlLayerState | null): Promise => { - if (prevState && prevState === state) { - this.log.trace('State unchanged, skipping update'); - return; - } - - if (!prevState || state.isEnabled !== prevState.isEnabled) { - this.log.trace('Updating visibility'); - this.konva.layer.visible(state.isEnabled); - this.renderer.syncCache(state.isEnabled); - } - if (!prevState || state.isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (!prevState || state.objects !== prevState.objects) { - const didRender = await this.renderer.render(); - if (didRender) { - this.transformer.requestRectCalculation(); - } - } - if (!prevState || state.position !== prevState.position) { - this.transformer.updatePosition(); - } - if (!prevState || state.opacity !== prevState.opacity) { - this.renderer.updateOpacity(); - } - if (!prevState || state.withTransparencyEffect !== prevState.withTransparencyEffect) { - this.renderer.updateTransparencyEffect(); - } - - if (!prevState) { - // First render - this.transformer.updateBbox(); - } - }; - - getCanvas = (rect?: Rect): HTMLCanvasElement => { - this.log.trace({ rect }, 'Getting canvas'); - // The opacity may have been changed in response to user selecting a different entity category, so we must restore - // the original opacity before rendering the canvas - const attrs: GroupConfig = { opacity: this.state.opacity }; - const canvas = this.renderer.getCanvas(rect, attrs); - return canvas; - }; - - getHashableState = (): SerializableObject => { - const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect']; - return omit(this.state, keysToOmit); - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts index dea9af3b3c..f84c6b3623 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts @@ -4,10 +4,12 @@ import { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasE import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, CanvasRenderableEntityState, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; import stableHash from 'stable-hash'; +import { assert } from 'tsafe'; export abstract class CanvasEntityAdapterBase< T extends CanvasRenderableEntityState = CanvasRenderableEntityState, @@ -41,6 +43,16 @@ export abstract class CanvasEntityAdapterBase< */ renderer: CanvasEntityObjectRenderer; + /** + * The entity's state. + */ + state: T; + + /** + * A set of subscriptions to stores. + */ + subscriptions = new Set<() => void>(); + constructor(entityIdentifier: CanvasEntityIdentifier, manager: CanvasManager, adapterType: string) { super(); this.type = adapterType; @@ -61,22 +73,101 @@ export abstract class CanvasEntityAdapterBase< }), }; + this.manager.stage.addLayer(this.konva.layer); + + // On creation, we need to get the latest snapshot of the entity's state from the store. + const initialState = this.getSnapshot(); + assert(initialState !== undefined, 'Missing entity state on creation'); + this.state = initialState; + this.renderer = new CanvasEntityObjectRenderer(this); this.transformer = new CanvasEntityTransformer(this); } - abstract get state(): T; - - abstract set state(state: T); - - abstract getCanvas: (rect?: Rect) => HTMLCanvasElement; - - abstract getHashableState: () => SerializableObject; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; + /** + * Gets the latest snapshot of the entity's state from the store. If the entity does not exist, returns undefined. + */ + getSnapshot = (): T | undefined => { + return selectEntity(this.manager.stateApi.getCanvasState(), this.entityIdentifier) as T | undefined; }; + /** + * Syncs the entity state with the canvas. This includes rendering the entity's objects, handling visibility, + * positioning, opacity, locked state, and any other properties. + * + * Implementations should be minimal and should only update the canvas if the state has changed. However, if `force` + * is true, the entity should be updated regardless of whether the state has changed. + * + * If the entity cannot be rendered, it should be destroyed. + */ + abstract sync: (force?: boolean) => void; + + /** + * Gets the canvas element for the entity. If `rect` is provided, the canvas will be clipped to that rectangle. + */ + abstract getCanvas: (rect?: Rect) => HTMLCanvasElement; + + /** + * Gets a hashable representation of the entity's state. + */ + abstract getHashableState: () => SerializableObject; + + /** + * Synchronizes the enabled state of the entity with the canvas. + */ + syncIsEnabled = () => { + this.log.trace('Updating visibility'); + this.konva.layer.visible(this.state.isEnabled); + this.renderer.syncCache(this.state.isEnabled); + }; + + /** + * Synchronizes the entity's objects with the canvas. + */ + syncObjects = () => { + this.renderer.render().then((didRender) => { + if (didRender) { + // If the objects have changed, we need to recalculate the transformer's bounding box. + this.transformer.requestRectCalculation(); + } + }); + }; + + /** + * Synchronizes the entity's position with the canvas. + */ + syncPosition = () => { + this.transformer.updatePosition(); + }; + + /** + * Synchronizes the entity's opacity with the canvas. + */ + syncOpacity = () => { + this.renderer.updateOpacity(); + }; + + /** + * Synchronizes the entity's locked state with the canvas. + */ + syncIsLocked = () => { + // The only thing we need to do is update the transformer's interaction state. For tool interactions, like drawing + // shapes, we defer to the CanvasToolModule to handle the locked state. + this.transformer.syncInteractionState(); + }; + + /** + * Checks if the entity is interactable. An entity is interactable if it is enabled, not locked, and its type is not + * hidden. + */ + getIsInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked && !this.manager.stateApi.getIsTypeHidden(this.state.type); + }; + + /** + * Gets a hash of the entity's state, as provided by `getHashableState`. If `extra` is provided, it will be included in + * the hash. + */ hash = (extra?: SerializableObject): string => { const arg = { state: this.getHashableState(), @@ -87,9 +178,12 @@ export abstract class CanvasEntityAdapterBase< destroy = (): void => { this.log.debug('Destroying module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); this.renderer.destroy(); this.transformer.destroy(); this.konva.layer.destroy(); + this.manager.deleteAdapter(this.entityIdentifier); }; repr = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterControlLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterControlLayer.ts new file mode 100644 index 0000000000..fed9ad9242 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterControlLayer.ts @@ -0,0 +1,69 @@ +import type { SerializableObject } from 'common/types'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasControlLayerState, CanvasEntityIdentifier, Rect } from 'features/controlLayers/store/types'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; + +export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase { + static TYPE = 'control_layer_adapter'; + + constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) { + super(entityIdentifier, manager, CanvasEntityAdapterControlLayer.TYPE); + this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync)); + this.sync(true); + } + + sync = (force?: boolean) => { + const prevState = this.state; + const state = this.getSnapshot(); + + if (!state) { + this.destroy(); + return; + } + + this.state = state; + + if (!force && prevState === this.state) { + return; + } + + if (force || this.state.isEnabled !== prevState.isEnabled) { + this.syncIsEnabled(); + } + if (force || this.state.isLocked !== prevState.isLocked) { + this.syncIsLocked(); + } + if (force || this.state.objects !== prevState.objects) { + this.syncObjects(); + } + if (force || this.state.position !== prevState.position) { + this.syncPosition(); + } + if (force || this.state.opacity !== prevState.opacity) { + this.syncOpacity(); + } + if (force || this.state.withTransparencyEffect !== prevState.withTransparencyEffect) { + this.renderer.updateTransparencyEffect(); + } + }; + + syncTransparencyEffect = () => { + this.renderer.updateTransparencyEffect(); + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + this.log.trace({ rect }, 'Getting canvas'); + // The opacity may have been changed in response to user selecting a different entity category, so we must restore + // the original opacity before rendering the canvas + const attrs: GroupConfig = { opacity: this.state.opacity }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect']; + return omit(this.state, keysToOmit); + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterInpaintMask.ts new file mode 100644 index 0000000000..ca0b113649 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterInpaintMask.ts @@ -0,0 +1,75 @@ +import type { SerializableObject } from 'common/types'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasEntityIdentifier, CanvasInpaintMaskState, Rect } from 'features/controlLayers/store/types'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; + +export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase { + static TYPE = 'inpaint_mask_adapter'; + + constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) { + super(entityIdentifier, manager, CanvasEntityAdapterInpaintMask.TYPE); + this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync)); + this.sync(true); + } + + sync = (force?: boolean) => { + const prevState = this.state; + const state = this.getSnapshot(); + + if (!state) { + this.destroy(); + return; + } + + this.state = state; + + if (!force && prevState === this.state) { + return; + } + + if (force || this.state.isEnabled !== prevState.isEnabled) { + this.syncIsEnabled(); + } + if (force || this.state.isLocked !== prevState.isLocked) { + this.syncIsLocked(); + } + if (force || this.state.objects !== prevState.objects) { + this.syncObjects(); + } + if (force || this.state.position !== prevState.position) { + this.syncPosition(); + } + if (force || this.state.opacity !== prevState.opacity) { + this.syncOpacity(); + } + if (force || this.state.fill !== prevState.fill) { + this.syncCompositingRectFill(); + } + if (force) { + this.syncCompositingRectSize(); + } + }; + + syncCompositingRectSize = () => { + this.renderer.updateCompositingRectSize(); + }; + + syncCompositingRectFill = () => { + this.renderer.updateCompositingRectFill(); + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasInpaintMaskState)[] = ['fill', 'name', 'opacity']; + return omit(this.state, keysToOmit); + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + // The opacity may have been changed in response to user selecting a different entity category, and the mask regions + // should be fully opaque - set opacity to 1 before rendering the canvas + const attrs: GroupConfig = { opacity: 1 }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRasterLayer.ts new file mode 100644 index 0000000000..df408854a9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRasterLayer.ts @@ -0,0 +1,62 @@ +import type { SerializableObject } from 'common/types'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasEntityIdentifier, CanvasRasterLayerState, Rect } from 'features/controlLayers/store/types'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; + +export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase { + static TYPE = 'raster_layer_adapter'; + + constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) { + super(entityIdentifier, manager, CanvasEntityAdapterRasterLayer.TYPE); + this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync)); + this.sync(true); + } + + sync = (force?: boolean) => { + const prevState = this.state; + const state = this.getSnapshot(); + + if (!state) { + this.destroy(); + return; + } + + this.state = state; + + if (!force && prevState === this.state) { + return; + } + + if (force || this.state.isEnabled !== prevState.isEnabled) { + this.syncIsEnabled(); + } + if (force || this.state.isLocked !== prevState.isLocked) { + this.syncIsLocked(); + } + if (force || this.state.objects !== prevState.objects) { + this.syncObjects(); + } + if (force || this.state.position !== prevState.position) { + this.syncPosition(); + } + if (force || this.state.opacity !== prevState.opacity) { + this.syncOpacity(); + } + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + this.log.trace({ rect }, 'Getting canvas'); + // The opacity may have been changed in response to user selecting a different entity category, so we must restore + // the original opacity before rendering the canvas + const attrs: GroupConfig = { opacity: this.state.opacity }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name']; + return omit(this.state, keysToOmit); + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance.ts new file mode 100644 index 0000000000..a47e67753e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance.ts @@ -0,0 +1,75 @@ +import type { SerializableObject } from 'common/types'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasEntityIdentifier, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; + +export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase { + static TYPE = 'regional_guidance_adapter'; + + constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) { + super(entityIdentifier, manager, CanvasEntityAdapterRegionalGuidance.TYPE); + this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync)); + this.sync(true); + } + + sync = (force?: boolean) => { + const prevState = this.state; + const state = this.getSnapshot(); + + if (!state) { + this.destroy(); + return; + } + + this.state = state; + + if (!force && prevState === this.state) { + return; + } + + if (force || this.state.isEnabled !== prevState.isEnabled) { + this.syncIsEnabled(); + } + if (force || this.state.isLocked !== prevState.isLocked) { + this.syncIsLocked(); + } + if (force || this.state.objects !== prevState.objects) { + this.syncObjects(); + } + if (force || this.state.position !== prevState.position) { + this.syncPosition(); + } + if (force || this.state.opacity !== prevState.opacity) { + this.syncOpacity(); + } + if (force || this.state.fill !== prevState.fill) { + this.syncCompositingRectFill(); + } + if (force) { + this.syncCompositingRectSize(); + } + }; + + syncCompositingRectSize = () => { + this.renderer.updateCompositingRectSize(); + }; + + syncCompositingRectFill = () => { + this.renderer.updateCompositingRectFill(); + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasRegionalGuidanceState)[] = ['fill', 'name', 'opacity']; + return omit(this.state, keysToOmit); + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + // The opacity may have been changed in response to user selecting a different entity category, and the mask regions + // should be fully opaque - set opacity to 1 before rendering the canvas + const attrs: GroupConfig = { opacity: 1 }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityObjectRenderer.ts index 732ce6f643..47b7ccf1ae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityObjectRenderer.ts @@ -20,7 +20,6 @@ import type { CanvasEraserLineState, CanvasImageState, CanvasRectState, - Fill, Rect, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; @@ -234,9 +233,13 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase { } }; - updateCompositingRectFill = (fill: Fill) => { + updateCompositingRectFill = () => { this.log.trace('Updating compositing rect fill'); + assert(this.konva.compositing, 'Missing compositing rect'); + assert(this.parent.state.type === 'inpaint_mask' || this.parent.state.type === 'regional_guidance'); + + const fill = this.parent.state.fill; if (fill.style === 'solid') { this.konva.compositing.rect.setAttrs({ @@ -621,6 +624,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase { destroy = () => { this.log.debug('Destroying module'); this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); for (const renderer of this.renderers.values()) { renderer.destroy(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts index b7933c5c8a..4ce4b6db8f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts @@ -1,11 +1,6 @@ -import { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { CanvasSessionState } from 'features/controlLayers/store/canvasSessionSlice'; import { type CanvasState, getEntityIdentifier } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; @@ -54,65 +49,42 @@ export class CanvasEntityRendererModule extends CanvasModuleBase { this.renderControlLayers(prevState, state); this.renderRegionalGuidance(prevState, state); this.renderInpaintMasks(state, prevState); - this.renderBbox(state, prevState); this.arrangeEntities(state, prevState); this.manager.tool.syncCursorStyle(); }; - renderRasterLayers = async (state: CanvasState, prevState: CanvasState | null) => { + renderRasterLayers = (state: CanvasState, prevState: CanvasState | null) => { const adapterMap = this.manager.adapters.rasterLayers; if (!prevState || state.rasterLayers.isHidden !== prevState.rasterLayers.isHidden) { for (const adapter of adapterMap.values()) { - adapter.renderer.updateOpacity(); + adapter.syncOpacity(); } } if (!prevState || state.rasterLayers.entities !== prevState.rasterLayers.entities) { - for (const entityAdapter of adapterMap.values()) { - if (!state.rasterLayers.entities.find((l) => l.id === entityAdapter.id)) { - await entityAdapter.destroy(); - adapterMap.delete(entityAdapter.id); - } - } - for (const entityState of state.rasterLayers.entities) { - let adapter = adapterMap.get(entityState.id); - if (!adapter) { - adapter = new CanvasRasterLayerAdapter(getEntityIdentifier(entityState), this.manager); - adapterMap.set(adapter.id, adapter); - this.manager.stage.addLayer(adapter.konva.layer); + if (!adapterMap.has(entityState.id)) { + this.manager.createAdapter(getEntityIdentifier(entityState)); } - adapter.state = entityState; } } }; - renderControlLayers = async (prevState: CanvasState | null, state: CanvasState) => { + renderControlLayers = (prevState: CanvasState | null, state: CanvasState) => { const adapterMap = this.manager.adapters.controlLayers; if (!prevState || state.controlLayers.isHidden !== prevState.controlLayers.isHidden) { for (const adapter of adapterMap.values()) { - adapter.renderer.updateOpacity(); + adapter.syncOpacity(); } } if (!prevState || state.controlLayers.entities !== prevState.controlLayers.entities) { - for (const entityAdapter of adapterMap.values()) { - if (!state.controlLayers.entities.find((l) => l.id === entityAdapter.id)) { - await entityAdapter.destroy(); - adapterMap.delete(entityAdapter.id); - } - } - for (const entityState of state.controlLayers.entities) { - let adapter = adapterMap.get(entityState.id); - if (!adapter) { - adapter = new CanvasControlLayerAdapter(getEntityIdentifier(entityState), this.manager); - adapterMap.set(adapter.id, adapter); - this.manager.stage.addLayer(adapter.konva.layer); + if (!adapterMap.has(entityState.id)) { + this.manager.createAdapter(getEntityIdentifier(entityState)); } - adapter.state = entityState; } } }; @@ -122,31 +94,15 @@ export class CanvasEntityRendererModule extends CanvasModuleBase { if (!prevState || state.regions.isHidden !== prevState.regions.isHidden) { for (const adapter of adapterMap.values()) { - adapter.renderer.updateOpacity(); + adapter.syncOpacity(); } } - if ( - !prevState || - state.regions.entities !== prevState.regions.entities || - state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id - ) { - // Destroy the konva nodes for nonexistent entities - for (const canvasRegion of adapterMap.values()) { - if (!state.regions.entities.find((rg) => rg.id === canvasRegion.id)) { - canvasRegion.destroy(); - adapterMap.delete(canvasRegion.id); - } - } - + if (!prevState || state.regions.entities !== prevState.regions.entities) { for (const entityState of state.regions.entities) { - let adapter = adapterMap.get(entityState.id); - if (!adapter) { - adapter = new CanvasRegionalGuidanceAdapter(getEntityIdentifier(entityState), this.manager); - adapterMap.set(adapter.id, adapter); - this.manager.stage.addLayer(adapter.konva.layer); + if (!adapterMap.has(entityState.id)) { + this.manager.createAdapter(getEntityIdentifier(entityState)); } - adapter.state = entityState; } } }; @@ -156,47 +112,19 @@ export class CanvasEntityRendererModule extends CanvasModuleBase { if (!prevState || state.inpaintMasks.isHidden !== prevState.inpaintMasks.isHidden) { for (const adapter of adapterMap.values()) { - adapter.renderer.updateOpacity(); + adapter.syncOpacity(); } } - if ( - !prevState || - state.inpaintMasks.entities !== prevState.inpaintMasks.entities || - state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id - ) { - // Destroy the konva nodes for nonexistent entities - for (const adapter of adapterMap.values()) { - if (!state.inpaintMasks.entities.find((rg) => rg.id === adapter.id)) { - adapter.destroy(); - adapterMap.delete(adapter.id); - } - } - + if (!prevState || state.inpaintMasks.entities !== prevState.inpaintMasks.entities) { for (const entityState of state.inpaintMasks.entities) { - let adapter = adapterMap.get(entityState.id); - if (!adapter) { - adapter = new CanvasInpaintMaskAdapter(getEntityIdentifier(entityState), this.manager); - adapterMap.set(adapter.id, adapter); - this.manager.stage.addLayer(adapter.konva.layer); + if (!adapterMap.has(entityState.id)) { + this.manager.createAdapter(getEntityIdentifier(entityState)); } - adapter.state = entityState; } } }; - renderBbox = (state: CanvasState, prevState: CanvasState | null) => { - if (!prevState || state.bbox !== prevState.bbox) { - this.manager.bbox.render(); - } - }; - - renderStagingArea = async (session: CanvasSessionState, prevSession: CanvasSessionState | null) => { - if (!prevSession || session !== prevSession) { - await this.manager.stagingArea.render(); - } - }; - arrangeEntities = (state: CanvasState, prevState: CanvasState | null) => { if ( !prevState || diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 0c63901000..677c16f10a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -472,7 +472,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { const tool = this.manager.tool.$tool.get(); const isSelected = this.manager.stateApi.getIsSelected(this.parent.id); - if (!this.parent.renderer.hasObjects() || !this.parent.isInteractable()) { + if (!this.parent.renderer.hasObjects() || !this.parent.getIsInteractable()) { // The layer is totally empty, we can just disable the layer this.parent.konva.layer.listening(false); this.setInteractionMode('off'); @@ -788,6 +788,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { destroy = () => { this.log.debug('Destroying module'); this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); this.konva.outlineRect.destroy(); this.konva.transformer.destroy(); this.konva.proxyRect.destroy(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index 382ac92b8b..8f8f836dc3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -1,10 +1,10 @@ import type { SerializableObject } from 'common/types'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntityAdapterControlLayer'; +import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntityAdapterInpaintMask'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; +import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntityAdapterRasterLayer'; +import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance'; 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'; @@ -25,10 +25,10 @@ export class CanvasFilterModule extends CanvasModuleBase { imageState: CanvasImageState | null = null; $adapter = atom< - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter + | CanvasEntityAdapterRasterLayer + | CanvasEntityAdapterControlLayer + | CanvasEntityAdapterInpaintMask + | CanvasEntityAdapterRegionalGuidance | null >(null); $isFiltering = computed(this.$adapter, (adapter) => Boolean(adapter)); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts deleted file mode 100644 index 6e3445a46a..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasEntityIdentifier, CanvasInpaintMaskState, Rect } from 'features/controlLayers/store/types'; -import type { GroupConfig } from 'konva/lib/Group'; -import { omit } from 'lodash-es'; -import { assert } from 'tsafe'; - -export class CanvasInpaintMaskAdapter extends CanvasEntityAdapterBase { - static TYPE = 'inpaint_mask_adapter'; - - /** - * The last known state of the entity. - */ - private _state: CanvasInpaintMaskState | null = null; - - constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) { - super(entityIdentifier, manager, CanvasInpaintMaskAdapter.TYPE); - } - - get state(): CanvasInpaintMaskState { - if (this._state) { - return this._state; - } - const state = this.manager.stateApi.getInpaintMasksState().entities.find((layer) => layer.id === this.id); - assert(state, `State not found for ${this.id}`); - return state; - } - - set state(state: CanvasInpaintMaskState) { - this._state = state; - } - - update = async (state: CanvasInpaintMaskState) => { - const prevState = this.state; - this.state = state; - - if (prevState && prevState === state) { - this.log.trace('State unchanged, skipping update'); - return; - } - - if (!prevState || state.isEnabled !== prevState.isEnabled) { - this.log.trace('Updating visibility'); - this.konva.layer.visible(state.isEnabled); - this.renderer.syncCache(state.isEnabled); - } - if (!prevState || state.objects !== prevState.objects) { - const didRender = await this.renderer.render(); - if (didRender) { - this.transformer.requestRectCalculation(); - } - } - if (!prevState || state.position !== prevState.position) { - this.transformer.updatePosition(); - } - if (!prevState || state.opacity !== prevState.opacity) { - this.renderer.updateOpacity(); - } - if (!prevState || state.isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (!prevState || state.fill !== prevState.fill) { - this.renderer.updateCompositingRectFill(state.fill); - } - - if (!prevState) { - this.renderer.updateCompositingRectSize(); - } - - if (!prevState) { - this.transformer.updateBbox(); - } - }; - - getHashableState = (): SerializableObject => { - const keysToOmit: (keyof CanvasInpaintMaskState)[] = ['fill', 'name', 'opacity']; - return omit(this.state, keysToOmit); - }; - - getCanvas = (rect?: Rect): HTMLCanvasElement => { - // The opacity may have been changed in response to user selecting a different entity category, and the mask regions - // should be fully opaque - set opacity to 1 before rendering the canvas - const attrs: GroupConfig = { opacity: 1 }; - const canvas = this.renderer.getCanvas(rect, attrs); - return canvas; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 3ebe26a39b..2c98bd8527 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,23 +6,31 @@ import { SyncableMap } from 'common/util/SyncableMap/SyncableMap'; import { CanvasBboxModule } from 'features/controlLayers/konva/CanvasBboxModule'; import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule'; import { CanvasCompositorModule } from 'features/controlLayers/konva/CanvasCompositorModule'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntityAdapterControlLayer'; +import { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntityAdapterInpaintMask'; +import { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntityAdapterRasterLayer'; +import { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance'; import { CanvasEntityRendererModule } from 'features/controlLayers/konva/CanvasEntityRendererModule'; import { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { CanvasStageModule } from 'features/controlLayers/konva/CanvasStageModule'; import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule'; import { CanvasToolModule } from 'features/controlLayers/konva/CanvasToolModule'; import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { + type CanvasEntityIdentifier, + isControlLayerEntityIdentifier, + isInpaintMaskEntityIdentifier, + isRasterLayerEntityIdentifier, + isRegionalGuidanceEntityIdentifier, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Atom } from 'nanostores'; import { atom, computed } from 'nanostores'; import type { Logger } from 'roarr'; +import { assert } from 'tsafe'; import { CanvasBackgroundModule } from './CanvasBackgroundModule'; import { CanvasStateApiModule } from './CanvasStateApiModule'; @@ -41,23 +49,10 @@ export class CanvasManager extends CanvasModuleBase { socket: AppSocket; adapters = { - rasterLayers: new SyncableMap(), - controlLayers: new SyncableMap(), - regionMasks: new SyncableMap(), - inpaintMasks: new SyncableMap(), - getAll: (): ( - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasRegionalGuidanceAdapter - | CanvasInpaintMaskAdapter - )[] => { - return [ - ...this.adapters.rasterLayers.values(), - ...this.adapters.controlLayers.values(), - ...this.adapters.regionMasks.values(), - ...this.adapters.inpaintMasks.values(), - ]; - }, + rasterLayers: new SyncableMap(), + controlLayers: new SyncableMap(), + regionMasks: new SyncableMap(), + inpaintMasks: new SyncableMap(), }; stateApi: CanvasStateApiModule; @@ -137,6 +132,53 @@ export class CanvasManager extends CanvasModuleBase { }); } + deleteAdapter = (entityIdentifier: CanvasEntityIdentifier): boolean => { + switch (entityIdentifier.type) { + case 'raster_layer': + return this.adapters.rasterLayers.delete(entityIdentifier.id); + case 'control_layer': + return this.adapters.controlLayers.delete(entityIdentifier.id); + case 'regional_guidance': + return this.adapters.regionMasks.delete(entityIdentifier.id); + case 'inpaint_mask': + return this.adapters.inpaintMasks.delete(entityIdentifier.id); + default: + return false; + } + }; + + getAllAdapters = (): ( + | CanvasEntityAdapterRasterLayer + | CanvasEntityAdapterControlLayer + | CanvasEntityAdapterRegionalGuidance + | CanvasEntityAdapterInpaintMask + )[] => { + return [ + ...this.adapters.rasterLayers.values(), + ...this.adapters.controlLayers.values(), + ...this.adapters.regionMasks.values(), + ...this.adapters.inpaintMasks.values(), + ]; + }; + + createAdapter = (entityIdentifier: CanvasEntityIdentifier): void => { + if (isRasterLayerEntityIdentifier(entityIdentifier)) { + const adapter = new CanvasEntityAdapterRasterLayer(entityIdentifier, this); + this.adapters.rasterLayers.set(adapter.id, adapter); + } else if (isControlLayerEntityIdentifier(entityIdentifier)) { + const adapter = new CanvasEntityAdapterControlLayer(entityIdentifier, this); + this.adapters.controlLayers.set(adapter.id, adapter); + } else if (isRegionalGuidanceEntityIdentifier(entityIdentifier)) { + const adapter = new CanvasEntityAdapterRegionalGuidance(entityIdentifier, this); + this.adapters.regionMasks.set(adapter.id, adapter); + } else if (isInpaintMaskEntityIdentifier(entityIdentifier)) { + const adapter = new CanvasEntityAdapterInpaintMask(entityIdentifier, this); + this.adapters.inpaintMasks.set(adapter.id, adapter); + } else { + assert(false, 'Unhandled entity type'); + } + }; + enableDebugging() { this._isDebugging = true; this.logDebugInfo(); @@ -163,7 +205,7 @@ export class CanvasManager extends CanvasModuleBase { destroy = () => { this.log.debug('Destroying module'); - for (const adapter of this.adapters.getAll()) { + for (const adapter of this.getAllAdapters()) { adapter.destroy(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts index 84296d7c08..bd6f21e11b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts @@ -101,6 +101,7 @@ export abstract class CanvasModuleBase { * destroy = () => { * this.log('Destroying module'); * this.subscriptions.forEach((unsubscribe) => unsubscribe()); + * this.subscriptions.clear(); * this.konva.group.destroy(); * }; * ``` diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts deleted file mode 100644 index 5beefbf823..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasEntityIdentifier, CanvasRasterLayerState, Rect } from 'features/controlLayers/store/types'; -import type { GroupConfig } from 'konva/lib/Group'; -import { omit } from 'lodash-es'; -import { assert } from 'tsafe'; - -export class CanvasRasterLayerAdapter extends CanvasEntityAdapterBase { - static TYPE = 'raster_layer_adapter'; - /** - * The last known state of the entity. - */ - private _state: CanvasRasterLayerState | null = null; - - constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) { - super(entityIdentifier, manager, CanvasRasterLayerAdapter.TYPE); - } - - get state(): CanvasRasterLayerState { - if (this._state) { - return this._state; - } - const state = this.manager.stateApi.getRasterLayersState().entities.find((layer) => layer.id === this.id); - assert(state, `State not found for ${this.id}`); - return state; - } - - set state(state: CanvasRasterLayerState) { - const prevState = this._state; - this._state = state; - this.render(state, prevState); - } - - private render = async (state: CanvasRasterLayerState, prevState: CanvasRasterLayerState | null): Promise => { - if (prevState && prevState === state) { - this.log.trace('State unchanged, skipping update'); - return; - } - - if (!prevState || state.isEnabled !== prevState.isEnabled) { - this.log.trace('Updating visibility'); - this.konva.layer.visible(state.isEnabled); - this.renderer.syncCache(state.isEnabled); - } - if (!prevState || state.isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (!prevState || state.objects !== prevState.objects) { - const didRender = await this.renderer.render(); - if (didRender) { - this.transformer.requestRectCalculation(); - } - } - if (!prevState || state.position !== prevState.position) { - this.transformer.updatePosition(); - } - if (!prevState || state.opacity !== prevState.opacity) { - this.renderer.updateOpacity(); - } - if (!prevState) { - // First render - this.transformer.updateBbox(); - } - }; - - getCanvas = (rect?: Rect): HTMLCanvasElement => { - this.log.trace({ rect }, 'Getting canvas'); - // The opacity may have been changed in response to user selecting a different entity category, so we must restore - // the original opacity before rendering the canvas - const attrs: GroupConfig = { opacity: this.state.opacity }; - const canvas = this.renderer.getCanvas(rect, attrs); - return canvas; - }; - - getHashableState = (): SerializableObject => { - const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name']; - return omit(this.state, keysToOmit); - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts deleted file mode 100644 index f16a453030..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasEntityIdentifier, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; -import type { GroupConfig } from 'konva/lib/Group'; -import { omit } from 'lodash-es'; -import { assert } from 'tsafe'; - -export class CanvasRegionalGuidanceAdapter extends CanvasEntityAdapterBase { - static TYPE = 'regional_guidance_adapter'; - - /** - * The last known state of the entity. - */ - private _state: CanvasRegionalGuidanceState | null = null; - - constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) { - super(entityIdentifier, manager, CanvasRegionalGuidanceAdapter.TYPE); - } - - get state(): CanvasRegionalGuidanceState { - if (this._state) { - return this._state; - } - const state = this.manager.stateApi.getRegionsState().entities.find((layer) => layer.id === this.id); - assert(state, `State not found for ${this.id}`); - return state; - } - - set state(state: CanvasRegionalGuidanceState) { - const prevState = this._state; - this._state = state; - this.render(state, prevState); - } - - render = async (state: CanvasRegionalGuidanceState, prevState: CanvasRegionalGuidanceState | null) => { - if (prevState && prevState === state) { - this.log.trace('State unchanged, skipping update'); - return; - } - - if (!prevState || state.isEnabled !== prevState.isEnabled) { - this.log.trace('Updating visibility'); - this.konva.layer.visible(state.isEnabled); - this.renderer.syncCache(state.isEnabled); - } - if (!prevState || state.objects !== prevState.objects) { - const didRender = await this.renderer.render(); - if (didRender) { - this.transformer.requestRectCalculation(); - } - } - if (!prevState || state.position !== prevState.position) { - this.transformer.updatePosition(); - } - if (!prevState || state.opacity !== prevState.opacity) { - this.renderer.updateOpacity(); - } - if (!prevState || state.isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (!prevState || state.fill !== prevState.fill) { - this.renderer.updateCompositingRectFill(state.fill); - } - - if (!prevState) { - this.renderer.updateCompositingRectSize(); - } - - if (!prevState) { - this.transformer.updateBbox(); - } - }; - - getHashableState = (): SerializableObject => { - const keysToOmit: (keyof CanvasRegionalGuidanceState)[] = ['fill', 'name', 'opacity']; - return omit(this.state, keysToOmit); - }; - - getCanvas = (rect?: Rect): HTMLCanvasElement => { - // The opacity may have been changed in response to user selecting a different entity category, and the mask regions - // should be fully opaque - set opacity to 1 before rendering the canvas - const attrs: GroupConfig = { opacity: 1 }; - const canvas = this.renderer.getCanvas(rect, attrs); - return canvas; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index d35cb0daaa..67930ffbb8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -116,7 +116,7 @@ export class CanvasStageModule extends CanvasModuleBase { getVisibleRect = (type?: Exclude): Rect => { const rects = []; - for (const adapter of this.manager.adapters.getAll()) { + for (const adapter of this.manager.getAllAdapters()) { if (!adapter.state.isEnabled) { continue; } @@ -322,6 +322,7 @@ export class CanvasStageModule extends CanvasModuleBase { destroy = () => { this.log.debug('Destroying module'); this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); this.konva.stage.destroy(); }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts index d0b5abd48c..5923e0e31f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts @@ -107,6 +107,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase { destroy = () => { this.log.debug('Destroying module'); this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); if (this.image) { this.image.destroy(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 8baf8515ce..37fa1b9e9b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -1,12 +1,12 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; import type { AppStore } from 'app/store/store'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntityAdapterControlLayer'; import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntityAdapterInpaintMask'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; +import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntityAdapterRasterLayer'; +import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntityAdapterRegionalGuidance'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; import { @@ -54,25 +54,25 @@ type EntityStateAndAdapter = id: string; type: CanvasRasterLayerState['type']; state: CanvasRasterLayerState; - adapter: CanvasRasterLayerAdapter; + adapter: CanvasEntityAdapterRasterLayer; } | { id: string; type: CanvasControlLayerState['type']; state: CanvasControlLayerState; - adapter: CanvasControlLayerAdapter; + adapter: CanvasEntityAdapterControlLayer; } | { id: string; type: CanvasInpaintMaskState['type']; state: CanvasInpaintMaskState; - adapter: CanvasInpaintMaskAdapter; + adapter: CanvasEntityAdapterInpaintMask; } | { id: string; type: CanvasRegionalGuidanceState['type']; state: CanvasRegionalGuidanceState; - adapter: CanvasRegionalGuidanceAdapter; + adapter: CanvasEntityAdapterRegionalGuidance; }; export class CanvasStateApiModule extends CanvasModuleBase { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 308ee5f7ed..7934dc3723 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -136,7 +136,6 @@ export class CanvasToolModule extends CanvasModuleBase { }; syncCursorStyle = () => { - this.log.trace('Syncing cursor style'); const stage = this.manager.stage; const isMouseDown = this.$isMouseDown.get(); const tool = this.$tool.get(); @@ -147,7 +146,7 @@ export class CanvasToolModule extends CanvasModuleBase { stage.setCursor('not-allowed'); } else if (this.manager.$isBusy.get()) { stage.setCursor('not-allowed'); - } else if (!this.manager.stateApi.getSelectedEntity()?.adapter.isInteractable()) { + } else if (!this.manager.stateApi.getSelectedEntity()?.adapter.getIsInteractable()) { stage.setCursor('not-allowed'); } else if (tool === 'colorPicker' || tool === 'brush' || tool === 'eraser') { stage.setCursor('none'); @@ -283,7 +282,7 @@ export class CanvasToolModule extends CanvasModuleBase { return false; } else if (this.manager.filter.$isFiltering.get()) { return false; - } else if (!this.manager.stateApi.getSelectedEntity()?.adapter.isInteractable()) { + } else if (!this.manager.stateApi.getSelectedEntity()?.adapter.getIsInteractable()) { return false; } else { return true; @@ -708,6 +707,7 @@ export class CanvasToolModule extends CanvasModuleBase { destroy = () => { this.log.debug('Destroying module'); this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions.clear(); this.konva.group.destroy(); }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index e7ce9e7a6f..82b91aa0d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -790,6 +790,30 @@ export function isDrawableEntityType( ); } +export function isRasterLayerEntityIdentifier( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is CanvasEntityIdentifier<'raster_layer'> { + return entityIdentifier.type === 'raster_layer'; +} + +export function isControlLayerEntityIdentifier( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is CanvasEntityIdentifier<'control_layer'> { + return entityIdentifier.type === 'control_layer'; +} + +export function isInpaintMaskEntityIdentifier( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is CanvasEntityIdentifier<'inpaint_mask'> { + return entityIdentifier.type === 'inpaint_mask'; +} + +export function isRegionalGuidanceEntityIdentifier( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is CanvasEntityIdentifier<'regional_guidance'> { + return entityIdentifier.type === 'regional_guidance'; +} + export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState { return isDrawableEntityType(entity.type); } @@ -803,5 +827,5 @@ export const getEntityIdentifier = ( export const isMaskEntityIdentifier = ( entityIdentifier: CanvasEntityIdentifier ): entityIdentifier is CanvasEntityIdentifier<'inpaint_mask' | 'regional_guidance'> => { - return entityIdentifier.type === 'inpaint_mask' || entityIdentifier.type === 'regional_guidance'; + return isInpaintMaskEntityIdentifier(entityIdentifier) || isRegionalGuidanceEntityIdentifier(entityIdentifier); };