diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx index fed4fcf816..c514e4ee8b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx @@ -22,7 +22,7 @@ import { selectEntity, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; -import { isDrawableEntity } from 'features/controlLayers/store/types'; +import { isRenderableEntity } from 'features/controlLayers/store/types'; import { clamp, round } from 'lodash-es'; import type { KeyboardEvent } from 'react'; import { memo, useCallback, useEffect, useState } from 'react'; @@ -70,7 +70,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => { if (!selectedEntity) { return 1; // fallback to 100% opacity } - if (!isDrawableEntity(selectedEntity)) { + if (!isRenderableEntity(selectedEntity)) { return 1; // fallback to 100% opacity } // Opacity is a float from 0-1, but we want to display it as a percentage diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx index bfb9e8a7d5..d9608788cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx @@ -1,86 +1,73 @@ import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; 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 { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi'; -const TransformBox = memo( - ({ - adapter, - }: { - adapter: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter; - }) => { - const { t } = useTranslation(); - const isProcessing = useStore(adapter.transformer.$isProcessing); +const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapterBase }) => { + const { t } = useTranslation(); + const isProcessing = useStore(adapter.transformer.$isProcessing); - return ( - - - {t('controlLayers.transform.transform')} - - - - - - - - - - ); - } -); + return ( + + + {t('controlLayers.transform.transform')} + + + + + + + + + + ); +}); TransformBox.displayName = 'Transform'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts index 576b592854..d9e200b3bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts @@ -1,73 +1,17 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasControlLayerState, CanvasEntityIdentifier, Rect } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasControlLayerAdapter extends CanvasModuleBase { - readonly type = 'control_layer_adapter'; - readonly id: string; - readonly path: string[]; - readonly manager: CanvasManager; - readonly parent: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'control_layer'>; - - /** - * The last known state of the entity. - */ +export class CanvasControlLayerAdapter extends CanvasEntityAdapterBase { + static TYPE = 'control_layer_adapter'; private _state: CanvasControlLayerState | null = null; - /** - * The Konva nodes that make up the entity layer: - * - A layer to hold the everything - * - * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. - */ - konva: { - layer: Konva.Layer; - }; - - /** - * The transformer for this entity layer. - */ - transformer: CanvasEntityTransformer; - - /** - * The renderer for this entity layer. - */ - renderer: CanvasEntityRenderer; - constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.manager = manager; - this.parent = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasControlLayerAdapter.TYPE); } get state(): CanvasControlLayerState { @@ -134,34 +78,4 @@ export class CanvasControlLayerAdapter extends CanvasModuleBase { const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect']; return omit(this.state, keysToOmit); }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - destroy = (): void => { - this.log.debug('Destroying module'); - this.renderer.destroy(); - this.transformer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - 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/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts new file mode 100644 index 0000000000..7a155b7773 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts @@ -0,0 +1,105 @@ +import type { SerializableObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasEntityIdentifier, CanvasRenderableEntityState, Rect } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { Logger } from 'roarr'; +import stableHash from 'stable-hash'; + +export abstract class CanvasEntityAdapterBase< + T extends CanvasRenderableEntityState = CanvasRenderableEntityState, +> extends CanvasModuleBase { + readonly type: string; + readonly id: string; + readonly path: string[]; + readonly manager: CanvasManager; + readonly parent: CanvasManager; + readonly log: Logger; + + readonly entityIdentifier: CanvasEntityIdentifier; + + /** + * The Konva nodes that make up the entity adapter: + * - A Konva.Layer to hold the everything + * + * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. + */ + konva: { + layer: Konva.Layer; + }; + + /** + * The transformer for this entity adapter. + */ + transformer: CanvasEntityTransformer; + + /** + * The renderer for this entity adapter. + */ + renderer: CanvasEntityRenderer; + + constructor(entityIdentifier: CanvasEntityIdentifier, manager: CanvasManager, adapterType: string) { + super(); + this.type = adapterType; + this.id = entityIdentifier.id; + this.entityIdentifier = entityIdentifier; + this.manager = manager; + this.parent = manager; + this.path = this.manager.buildPath(this); + this.log = this.manager.buildLogger(this); + + this.log.debug('Creating module'); + + this.konva = { + layer: new Konva.Layer({ + name: `${this.type}:layer`, + listening: false, + imageSmoothingEnabled: false, + }), + }; + + this.renderer = new CanvasEntityRenderer(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; + }; + + hash = (extra?: SerializableObject): string => { + const arg = { + state: this.getHashableState(), + extra, + }; + return stableHash(arg); + }; + + destroy = (): void => { + this.log.debug('Destroying module'); + this.renderer.destroy(); + this.transformer.destroy(); + this.konva.layer.destroy(); + }; + + repr = () => { + 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/CanvasEntityLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts deleted file mode 100644 index fa846b3cd0..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts +++ /dev/null @@ -1,228 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { getLastPointOfLine } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasControlLayerState, - CanvasEntityIdentifier, - CanvasEraserLineState, - CanvasRasterLayerState, - Coordinate, - Rect, -} from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { GroupConfig } from 'konva/lib/Group'; -import { get, omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; -import { assert } from 'tsafe'; - -/** - * Handles the rendering for a single raster or control layer entity. - * - * This module has two main components: - * - A transformer, which handles the positioning and interaction state of the layer - * - A renderer, which handles the rendering of the layer's objects - * - * The canvas rendering module interacts with this module to coordinate the rendering of all raster and control layers. - */ -export class CanvasEntityLayerAdapter extends CanvasModuleBase { - readonly type = 'entity_layer_adapter'; - readonly id: string; - readonly path: string[]; - readonly manager: CanvasManager; - readonly parent: CanvasManager; - readonly log: Logger; - - /** - * The last known state of the entity. - */ - state: CanvasRasterLayerState | CanvasControlLayerState; - - /** - * The Konva nodes that make up the entity layer: - * - A layer to hold the everything - * - * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. - */ - konva: { - layer: Konva.Layer; - }; - - /** - * The transformer for this entity layer. - */ - transformer: CanvasEntityTransformer; - - /** - * The renderer for this entity layer. - */ - renderer: CanvasEntityRenderer; - - /** - * Whether this is the first render of the entity layer. - */ - isFirstRender: boolean = true; - - constructor(state: CanvasEntityLayerAdapter['state'], manager: CanvasEntityLayerAdapter['manager']) { - super(); - this.id = state.id; - this.manager = manager; - this.parent = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.state = state; - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); - } - - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - - update = async (arg?: { state: CanvasEntityLayerAdapter['state'] }): Promise => { - const state = get(arg, 'state', this.state); - - const prevState = this.state; - this.state = state; - - if (!this.isFirstRender && prevState === state) { - this.log.trace('State unchanged, skipping update'); - return; - } - - this.log.debug('Updating'); - const { position, objects, opacity, isEnabled, isLocked } = state; - - if (this.isFirstRender || isEnabled !== prevState.isEnabled) { - this.updateVisibility({ isEnabled }); - } - if (this.isFirstRender || isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (this.isFirstRender || objects !== prevState.objects) { - await this.updateObjects({ objects }); - } - if (this.isFirstRender || position !== prevState.position) { - this.transformer.updatePosition({ position }); - } - if (this.isFirstRender || opacity !== prevState.opacity) { - this.renderer.updateOpacity(opacity); - } - - if (state.type === 'control_layer' && prevState.type === 'control_layer') { - if (this.isFirstRender || state.withTransparencyEffect !== prevState.withTransparencyEffect) { - this.renderer.updateTransparencyEffect(state.withTransparencyEffect); - } - } - - if (this.isFirstRender) { - this.transformer.updateBbox(); - } - - this.isFirstRender = false; - }; - - updateVisibility = (arg?: { isEnabled: boolean }) => { - this.log.trace('Updating visibility'); - const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - this.konva.layer.visible(isEnabled); - this.renderer.syncCache(isEnabled); - }; - - updateObjects = async (arg?: { objects: CanvasRasterLayerState['objects'] }) => { - this.log.trace('Updating objects'); - - const objects = get(arg, 'objects', this.state.objects); - - const didUpdate = await this.renderer.render(objects); - - if (didUpdate) { - this.transformer.requestRectCalculation(); - } - }; - - 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 => { - if (this.state.type === 'control_layer') { - const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect']; - return omit(this.state, keysToOmit); - } else if (this.state.type === 'raster_layer') { - const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name']; - return omit(this.state, keysToOmit); - } else { - assert(false, 'Unexpected layer type'); - } - }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => { - const lastObject = this.state.objects[this.state.objects.length - 1]; - if (!lastObject) { - return null; - } - - if (lastObject.type === type) { - return getLastPointOfLine(lastObject.points); - } - - return null; - }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = (): void => { - this.log.debug('Destroying module'); - this.renderer.destroy(); - this.transformer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - 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/CanvasEntityMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts deleted file mode 100644 index eb5a4fe5fb..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { getLastPointOfLine } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasEntityIdentifier, - CanvasEraserLineState, - CanvasInpaintMaskState, - CanvasRegionalGuidanceState, - Coordinate, - Rect, -} from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { GroupConfig } from 'konva/lib/Group'; -import { get, omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; - -export class CanvasEntityMaskAdapter extends CanvasModuleBase { - readonly type = 'entity_mask_adapter'; - readonly id: string; - readonly path: string[]; - readonly parent: CanvasManager; - readonly manager: CanvasManager; - readonly log: Logger; - - state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; - - transformer: CanvasEntityTransformer; - renderer: CanvasEntityRenderer; - - isFirstRender: boolean = true; - - konva: { - layer: Konva.Layer; - }; - - constructor(state: CanvasEntityMaskAdapter['state'], manager: CanvasEntityMaskAdapter['manager']) { - super(); - this.id = state.id; - this.parent = manager; - this.manager = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.state = state; - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); - } - - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - - update = async (arg?: { state: CanvasEntityMaskAdapter['state'] }) => { - const state = get(arg, 'state', this.state); - - const prevState = this.state; - this.state = state; - - if (!this.isFirstRender && prevState === state && prevState.fill === state.fill) { - this.log.trace('State unchanged, skipping update'); - return; - } - - this.log.debug('Updating'); - const { position, objects, isEnabled, isLocked, opacity } = state; - - if (this.isFirstRender || objects !== prevState.objects) { - await this.updateObjects({ objects }); - } - if (this.isFirstRender || position !== prevState.position) { - this.transformer.updatePosition({ position }); - } - if (this.isFirstRender || opacity !== prevState.opacity) { - this.renderer.updateOpacity(opacity); - } - if (this.isFirstRender || isEnabled !== prevState.isEnabled) { - this.updateVisibility({ isEnabled }); - } - if (this.isFirstRender || isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (this.isFirstRender || state.fill !== prevState.fill) { - this.renderer.updateCompositingRectFill(state.fill); - } - - if (this.isFirstRender) { - this.renderer.updateCompositingRectSize(); - } - - if (this.isFirstRender) { - this.transformer.updateBbox(); - } - - this.isFirstRender = false; - }; - - updateObjects = async (arg?: { objects: CanvasInpaintMaskState['objects'] }) => { - this.log.trace('Updating objects'); - - const objects = get(arg, 'objects', this.state.objects); - - const didUpdate = await this.renderer.render(objects); - - if (didUpdate) { - this.transformer.requestRectCalculation(); - } - }; - - updateVisibility = (arg?: { isEnabled: boolean }) => { - this.log.trace('Updating visibility'); - const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - this.konva.layer.visible(isEnabled); - }; - - getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => { - const lastObject = this.state.objects[this.state.objects.length - 1]; - if (!lastObject) { - return null; - } - - if (lastObject.type === type) { - return getLastPointOfLine(lastObject.points); - } - - return null; - }; - - getHashableState = (): SerializableObject => { - const keysToOmit: (keyof CanvasEntityMaskAdapter['state'])[] = ['fill', 'name', 'opacity']; - return omit(this.state, keysToOmit); - }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - 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; - }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = () => { - this.log.debug('Destroying module'); - this.transformer.destroy(); - this.renderer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - }; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts index 8c9f8e12d9..ee28a35ac4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts @@ -1,14 +1,11 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasObjectBrushLineRenderer } from 'features/controlLayers/konva/CanvasObjectBrushLineRenderer'; import { CanvasObjectEraserLineRenderer } from 'features/controlLayers/konva/CanvasObjectEraserLineRenderer'; import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasObjectImageRenderer'; import { CanvasObjectRectRenderer } from 'features/controlLayers/konva/CanvasObjectRectRenderer'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; import { @@ -66,11 +63,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { readonly type = 'entity_renderer'; readonly id: string; readonly path: string[]; - readonly parent: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter; + readonly parent: CanvasEntityAdapterBase; readonly manager: CanvasManager; readonly log: Logger; @@ -141,13 +134,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { */ $canvasCache = atom<{ canvas: HTMLCanvasElement; rect: Rect } | null>(null); - constructor( - parent: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter - ) { + constructor(parent: CanvasEntityAdapterBase) { super(); this.id = getPrefixedId(this.type); this.parent = parent; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 7c755624ad..0c63901000 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -1,9 +1,6 @@ -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; 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 { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, Rect, RectWithRotation } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -82,11 +79,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { readonly type = 'entity_transformer'; readonly id: string; readonly path: string[]; - readonly parent: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter; + readonly parent: CanvasEntityAdapterBase; readonly manager: CanvasManager; readonly log: Logger; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts index 1a02b8077c..0a0028b830 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts @@ -1,61 +1,21 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasEntityIdentifier, CanvasInpaintMaskState, Rect } from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasInpaintMaskAdapter extends CanvasModuleBase { - readonly type = 'inpaint_mask_adapter'; - readonly id: string; - readonly path: string[]; - readonly parent: CanvasManager; - readonly manager: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>; +export class CanvasInpaintMaskAdapter extends CanvasEntityAdapterBase { + static TYPE = 'inpaint_mask_adapter'; /** * The last known state of the entity. */ private _state: CanvasInpaintMaskState | null = null; - transformer: CanvasEntityTransformer; - renderer: CanvasEntityRenderer; - - konva: { - layer: Konva.Layer; - }; - constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.parent = manager; - this.manager = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasInpaintMaskAdapter.TYPE); } get state(): CanvasInpaintMaskState { @@ -71,13 +31,6 @@ export class CanvasInpaintMaskAdapter extends CanvasModuleBase { this._state = state; } - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - update = async (state: CanvasInpaintMaskState) => { const prevState = this.state; this.state = state; @@ -125,14 +78,6 @@ export class CanvasInpaintMaskAdapter extends CanvasModuleBase { return omit(this.state, keysToOmit); }; - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - 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 @@ -140,24 +85,4 @@ export class CanvasInpaintMaskAdapter extends CanvasModuleBase { const canvas = this.renderer.getCanvas(rect, attrs); return canvas; }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = () => { - this.log.debug('Destroying module'); - this.transformer.destroy(); - this.renderer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - }; - }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts index e1e8bbeecc..c3a9da71d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts @@ -1,73 +1,20 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasEntityIdentifier, CanvasRasterLayerState, Rect } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasRasterLayerAdapter extends CanvasModuleBase { - readonly type = 'raster_layer_adapter'; - readonly id: string; - readonly path: string[]; - readonly manager: CanvasManager; - readonly parent: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'raster_layer'>; - +export class CanvasRasterLayerAdapter extends CanvasEntityAdapterBase { + static TYPE = 'raster_layer_adapter'; /** * The last known state of the entity. */ private _state: CanvasRasterLayerState | null = null; - /** - * The Konva nodes that make up the entity layer: - * - A layer to hold the everything - * - * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. - */ - konva: { - layer: Konva.Layer; - }; - - /** - * The transformer for this entity layer. - */ - transformer: CanvasEntityTransformer; - - /** - * The renderer for this entity layer. - */ - renderer: CanvasEntityRenderer; - constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.manager = manager; - this.parent = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasRasterLayerAdapter.TYPE); } get state(): CanvasRasterLayerState { @@ -126,38 +73,8 @@ export class CanvasRasterLayerAdapter extends CanvasModuleBase { return canvas; }; - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - getHashableState = (): SerializableObject => { const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name']; return omit(this.state, keysToOmit); }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - destroy = (): void => { - this.log.debug('Destroying module'); - this.renderer.destroy(); - this.transformer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - 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/CanvasRegionalGuidanceAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts index e72dbfc174..5ce08fa789 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts @@ -1,61 +1,21 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasEntityIdentifier, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { - readonly type = 'regional_guidance_adapter'; - readonly id: string; - readonly path: string[]; - readonly parent: CanvasManager; - readonly manager: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>; +export class CanvasRegionalGuidanceAdapter extends CanvasEntityAdapterBase { + static TYPE = 'regional_guidance_adapter'; /** * The last known state of the entity. */ private _state: CanvasRegionalGuidanceState | null = null; - transformer: CanvasEntityTransformer; - renderer: CanvasEntityRenderer; - - konva: { - layer: Konva.Layer; - }; - constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.parent = manager; - this.manager = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasRegionalGuidanceAdapter.TYPE); } get state(): CanvasRegionalGuidanceState { @@ -68,20 +28,12 @@ export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { } set state(state: CanvasRegionalGuidanceState) { + const prevState = this._state; this._state = state; + this.render(state, prevState); } - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - - update = async (state: CanvasRegionalGuidanceState) => { - const prevState = this.state; - this.state = state; - + render = async (state: CanvasRegionalGuidanceState, prevState: CanvasRegionalGuidanceState | null) => { if (prevState && prevState === state) { this.log.trace('State unchanged, skipping update'); return; @@ -125,14 +77,6 @@ export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { return omit(this.state, keysToOmit); }; - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - 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 @@ -140,24 +84,4 @@ export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { const canvas = this.renderer.getCanvas(rect, attrs); return canvas; }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = () => { - this.log.debug('Destroying module'); - this.transformer.destroy(); - this.renderer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - }; - }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 0dd85b3819..b3fb091365 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -1,6 +1,7 @@ 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 { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; @@ -362,13 +363,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { /** * The entity adapter being transformed, if any. */ - $transformingAdapter = atom< - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter - | null - >(null); + $transformingAdapter = atom(null); /** * Whether an entity is currently being transformed. Derived from `$transformingAdapter`. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 0d3b01c91a..308ee5f7ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -24,7 +24,7 @@ import type { RgbColor, Tool, } from 'features/controlLayers/store/types'; -import { isDrawableEntity, RGBA_BLACK } from 'features/controlLayers/store/types'; +import { isRenderableEntity, RGBA_BLACK } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import { atom } from 'nanostores'; @@ -171,7 +171,7 @@ export class CanvasToolModule extends CanvasModuleBase { !!selectedEntity && selectedEntity.state.isEnabled && !selectedEntity.state.isLocked && - isDrawableEntity(selectedEntity.state); + isRenderableEntity(selectedEntity.state); this.syncCursorStyle(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index ba62226c3c..5987553f26 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -56,7 +56,7 @@ import { imageDTOToImageWithDims, initialControlNet, initialIPAdapter, - isDrawableEntity, + isRenderableEntity, } from './types'; const DEFAULT_MASK_COLORS: RgbColor[] = [ @@ -818,7 +818,7 @@ export const canvasSlice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (isDrawableEntity(entity)) { + } else if (isRenderableEntity(entity)) { entity.isEnabled = true; entity.objects = []; entity.position = { x: 0, y: 0 }; @@ -907,7 +907,7 @@ export const canvasSlice = createSlice({ return; } - if (isDrawableEntity(entity)) { + if (isRenderableEntity(entity)) { entity.position = position; } }, @@ -918,7 +918,7 @@ export const canvasSlice = createSlice({ return; } - if (isDrawableEntity(entity)) { + if (isRenderableEntity(entity)) { if (replaceObjects) { entity.objects = [imageObject]; entity.position = { x: rect.x, y: rect.y }; @@ -932,7 +932,7 @@ export const canvasSlice = createSlice({ return; } - if (!isDrawableEntity(entity)) { + if (!isRenderableEntity(entity)) { assert(false, `Cannot add a brush line to a non-drawable entity of type ${entity.type}`); } @@ -947,7 +947,7 @@ export const canvasSlice = createSlice({ return; } - if (!isDrawableEntity(entity)) { + if (!isRenderableEntity(entity)) { assert(false, `Cannot add a eraser line to a non-drawable entity of type ${entity.type}`); } @@ -962,7 +962,7 @@ export const canvasSlice = createSlice({ return; } - if (!isDrawableEntity(entity)) { + if (!isRenderableEntity(entity)) { assert(false, `Cannot add a rect to a non-drawable entity of type ${entity.type}`); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 048af3b7d5..e7ce9e7a6f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -675,6 +675,12 @@ export type CanvasEntityState = | CanvasInpaintMaskState | CanvasIPAdapterState; +export type CanvasRenderableEntityState = + | CanvasRasterLayerState + | CanvasControlLayerState + | CanvasRegionalGuidanceState + | CanvasInpaintMaskState; + export type CanvasEntityType = CanvasEntityState['type']; export type CanvasEntityIdentifier = { id: string; type: T }; @@ -773,7 +779,9 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{ export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; -export function isDrawableEntityType(entityType: CanvasEntityState['type']) { +export function isDrawableEntityType( + entityType: CanvasEntityState['type'] +): entityType is CanvasRenderableEntityState['type'] { return ( entityType === 'raster_layer' || entityType === 'control_layer' || @@ -782,9 +790,7 @@ export function isDrawableEntityType(entityType: CanvasEntityState['type']) { ); } -export function isDrawableEntity( - entity: CanvasEntityState -): entity is CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { +export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState { return isDrawableEntityType(entity.type); }