feat(ui): streamlined state flow

This commit is contained in:
psychedelicious
2024-09-05 14:38:58 +10:00
parent 4931bdace5
commit e176e48fa3
25 changed files with 557 additions and 502 deletions

View File

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

View File

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

View File

@@ -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(() => {

View File

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

View File

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

View File

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

View File

@@ -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<T['type']>, 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 = () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, CanvasRasterLayerAdapter>(),
controlLayers: new SyncableMap<string, CanvasControlLayerAdapter>(),
regionMasks: new SyncableMap<string, CanvasRegionalGuidanceAdapter>(),
inpaintMasks: new SyncableMap<string, CanvasInpaintMaskAdapter>(),
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<string, CanvasEntityAdapterRasterLayer>(),
controlLayers: new SyncableMap<string, CanvasEntityAdapterControlLayer>(),
regionMasks: new SyncableMap<string, CanvasEntityAdapterRegionalGuidance>(),
inpaintMasks: new SyncableMap<string, CanvasEntityAdapterInpaintMask>(),
};
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();
}

View File

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

View File

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

View File

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

View File

@@ -116,7 +116,7 @@ export class CanvasStageModule extends CanvasModuleBase {
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): 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();
};
}

View File

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

View File

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

View File

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

View File

@@ -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 = <T extends CanvasEntityType>(
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);
};