feat(ui): abstract out CanvasEntityAdapterBase

Things were getting to complex to reason about & classes a bit complicated. Trying to simplify...
This commit is contained in:
psychedelicious
2024-09-04 19:06:49 +10:00
parent 1c15c2cb03
commit 4e5f4dadf2
15 changed files with 214 additions and 878 deletions

View File

@@ -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

View File

@@ -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 (
<Flex
bg="base.800"
borderRadius="base"
p={4}
flexDir="column"
gap={4}
w={420}
h="auto"
shadow="dark-lg"
transitionProperty="height"
transitionDuration="normal"
>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.transform.transform')}
</Heading>
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
leftIcon={<PiArrowsOutBold />}
onClick={adapter.transformer.fitProxyRectToBbox}
isLoading={isProcessing}
loadingText={t('controlLayers.transform.reset')}
variant="ghost"
>
{t('controlLayers.transform.fitToBbox')}
</Button>
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.transformer.resetTransform}
isLoading={isProcessing}
loadingText={t('controlLayers.reset')}
variant="ghost"
>
{t('controlLayers.transform.reset')}
</Button>
<Button
leftIcon={<PiCheckBold />}
onClick={adapter.transformer.applyTransform}
isLoading={isProcessing}
loadingText={t('common.apply')}
variant="ghost"
>
{t('controlLayers.transform.apply')}
</Button>
<Button
leftIcon={<PiXBold />}
onClick={adapter.transformer.stopTransform}
isLoading={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"
>
{t('controlLayers.transform.cancel')}
</Button>
</ButtonGroup>
</Flex>
);
}
);
return (
<Flex
bg="base.800"
borderRadius="base"
p={4}
flexDir="column"
gap={4}
w={420}
h="auto"
shadow="dark-lg"
transitionProperty="height"
transitionDuration="normal"
>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.transform.transform')}
</Heading>
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
leftIcon={<PiArrowsOutBold />}
onClick={adapter.transformer.fitProxyRectToBbox}
isLoading={isProcessing}
loadingText={t('controlLayers.transform.reset')}
variant="ghost"
>
{t('controlLayers.transform.fitToBbox')}
</Button>
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.transformer.resetTransform}
isLoading={isProcessing}
loadingText={t('controlLayers.reset')}
variant="ghost"
>
{t('controlLayers.transform.reset')}
</Button>
<Button
leftIcon={<PiCheckBold />}
onClick={adapter.transformer.applyTransform}
isLoading={isProcessing}
loadingText={t('common.apply')}
variant="ghost"
>
{t('controlLayers.transform.apply')}
</Button>
<Button
leftIcon={<PiXBold />}
onClick={adapter.transformer.stopTransform}
isLoading={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"
>
{t('controlLayers.transform.cancel')}
</Button>
</ButtonGroup>
</Flex>
);
});
TransformBox.displayName = 'Transform';

View File

@@ -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<CanvasControlLayerState> {
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(),
};
};
}

View File

@@ -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<T['type']>;
/**
* 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<T['type']>, 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(),
};
};
}

View File

@@ -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<void> => {
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(),
};
};
}

View File

@@ -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),
};
};
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<CanvasInpaintMaskState> {
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),
};
};
}

View File

@@ -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<CanvasRasterLayerState> {
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(),
};
};
}

View File

@@ -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<CanvasRegionalGuidanceState> {
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),
};
};
}

View File

@@ -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<CanvasEntityAdapterBase | null>(null);
/**
* Whether an entity is currently being transformed. Derived from `$transformingAdapter`.

View File

@@ -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();

View File

@@ -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}`);
}

View File

@@ -675,6 +675,12 @@ export type CanvasEntityState =
| CanvasInpaintMaskState
| CanvasIPAdapterState;
export type CanvasRenderableEntityState =
| CanvasRasterLayerState
| CanvasControlLayerState
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState;
export type CanvasEntityType = CanvasEntityState['type'];
export type CanvasEntityIdentifier<T extends CanvasEntityType = CanvasEntityType> = { 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);
}