mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): iterate on state flow and rendering
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { createReduxSubscription, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
@@ -28,7 +30,7 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
private _dynamicGrid: boolean | null = null;
|
||||
dynamicGrid: boolean;
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
config: CanvasBackgroundModuleConfig = DEFAULT_CONFIG;
|
||||
@@ -62,26 +64,15 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
* - size
|
||||
*/
|
||||
this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render));
|
||||
this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync));
|
||||
}
|
||||
|
||||
sync = (): boolean => {
|
||||
this._dynamicGrid = this.manager.stateApi.getSettings().dynamicGrid;
|
||||
return this._dynamicGrid;
|
||||
};
|
||||
|
||||
get dynamicGrid(): boolean {
|
||||
if (this._dynamicGrid === null) {
|
||||
return this.sync();
|
||||
}
|
||||
return this._dynamicGrid;
|
||||
}
|
||||
|
||||
set dynamicGrid(dynamicGrid: boolean) {
|
||||
if (this._dynamicGrid !== dynamicGrid) {
|
||||
this._dynamicGrid = dynamicGrid;
|
||||
this.render();
|
||||
}
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (settings) => settings.dynamicGrid);
|
||||
this.dynamicGrid = selectDynamicGrid(this.manager.stateApi.store.getState());
|
||||
this.subscriptions.add(
|
||||
createReduxSubscription(this.manager.stateApi.store, selectDynamicGrid, (dynamicGrid) => {
|
||||
this.dynamicGrid = dynamicGrid;
|
||||
this.render();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { createReduxSubscription, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasState, Coordinate, Rect } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
@@ -64,10 +66,6 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
|
||||
this.log.debug('Creating bbox module');
|
||||
|
||||
// Set the initial aspect ratio buffer per app state.
|
||||
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 }),
|
||||
// We will use a Konva.Transformer for the generation bbox. Transformers need some shape to transform, so we will
|
||||
@@ -77,10 +75,6 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
draggable: true,
|
||||
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`,
|
||||
@@ -114,18 +108,20 @@ 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));
|
||||
|
||||
// Also listen to redux state to update the bbox's position and dimensions.
|
||||
const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
|
||||
this.state = selectBbox(this.manager.stateApi.store.getState());
|
||||
this.$aspectRatioBuffer.set(this.state.rect.width / this.state.rect.height);
|
||||
this.render();
|
||||
this.subscriptions.add(
|
||||
createReduxSubscription(this.manager.stateApi.store, selectBbox, (bbox) => {
|
||||
this.state = bbox;
|
||||
this.render();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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,7 +147,10 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.parent.konva.layer.add(this.konva.objectGroup);
|
||||
this.parent.konva.layer.add(this.konva.bufferGroup);
|
||||
|
||||
if (this.parent.state.type === 'inpaint_mask' || this.parent.state.type === 'regional_guidance') {
|
||||
if (
|
||||
this.parent.entityIdentifier.type === 'inpaint_mask' ||
|
||||
this.parent.entityIdentifier.type === 'regional_guidance'
|
||||
) {
|
||||
const rect = new Konva.Rect({
|
||||
name: `${this.type}:compositing_rect`,
|
||||
globalCompositeOperation: 'source-in',
|
||||
@@ -256,6 +259,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
|
||||
updateCompositingRectSize = () => {
|
||||
this.log.trace('Updating compositing rect size');
|
||||
|
||||
assert(this.konva.compositing, 'Missing compositing rect');
|
||||
|
||||
const { x, y, width, height, scale } = this.manager.stage.$stageAttrs.get();
|
||||
|
||||
@@ -4,7 +4,6 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'
|
||||
import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
readonly type = 'progress_image';
|
||||
@@ -23,7 +22,7 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
isError: boolean = false;
|
||||
imageElement: HTMLImageElement | null = null;
|
||||
|
||||
lastProgressEvent: S['InvocationDenoiseProgressEvent'] | null = null;
|
||||
subscriptions = new Set<() => void>();
|
||||
|
||||
mutex: Mutex = new Mutex();
|
||||
|
||||
@@ -42,10 +41,7 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
image: null,
|
||||
};
|
||||
|
||||
this.manager.stateApi.$lastCanvasProgressEvent.listen((event) => {
|
||||
this.lastProgressEvent = event;
|
||||
this.render();
|
||||
});
|
||||
this.subscriptions.add(this.manager.stateApi.$lastCanvasProgressEvent.listen(this.render));
|
||||
}
|
||||
|
||||
getNodes = () => {
|
||||
@@ -55,7 +51,9 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
render = async () => {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
if (!this.lastProgressEvent) {
|
||||
const event = this.manager.stateApi.$lastCanvasProgressEvent.get();
|
||||
|
||||
if (!event) {
|
||||
this.konva.group.visible(false);
|
||||
this.imageElement = null;
|
||||
this.isLoading = false;
|
||||
@@ -67,7 +65,7 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
this.isLoading = true;
|
||||
|
||||
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
|
||||
const { dataURL } = this.lastProgressEvent.progress_image;
|
||||
const { dataURL } = event.progress_image;
|
||||
try {
|
||||
this.imageElement = await loadImage(dataURL);
|
||||
if (this.konva.image) {
|
||||
@@ -101,6 +99,8 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this.subscriptions.clear();
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObjectImage';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasSessionState } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { createReduxSubscription, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { type CanvasSessionState, selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
@@ -16,7 +16,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
private _state: CanvasSessionState | null = null;
|
||||
state: CanvasSessionState;
|
||||
|
||||
subscriptions: Set<() => void> = new Set();
|
||||
konva: { group: Konva.Group };
|
||||
@@ -40,26 +40,14 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
this.selectedImage = null;
|
||||
|
||||
this.subscriptions.add(this.$shouldShowStagedImage.listen(this.render));
|
||||
this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync));
|
||||
}
|
||||
|
||||
sync = (): CanvasSessionState => {
|
||||
this.state = this.manager.stateApi.getSession();
|
||||
return this.state;
|
||||
};
|
||||
|
||||
get state(): CanvasSessionState {
|
||||
if (!this._state) {
|
||||
return this.manager.stateApi.getSession();
|
||||
}
|
||||
return this._state;
|
||||
}
|
||||
|
||||
set state(state: CanvasSessionState) {
|
||||
if (this._state !== state) {
|
||||
this._state = state;
|
||||
this.render();
|
||||
}
|
||||
this.state = selectCanvasSessionSlice(this.manager.stateApi.store.getState());
|
||||
this.subscriptions.add(
|
||||
createReduxSubscription(this.manager.stateApi.store, selectCanvasSessionSlice, (session) => {
|
||||
this.state = session;
|
||||
this.render();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render = async () => {
|
||||
|
||||
@@ -305,7 +305,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedEntity.renderer.bufferState?.type !== 'rect') {
|
||||
if (selectedEntity.renderer.bufferState?.type !== 'rect' && selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
return;
|
||||
}
|
||||
@@ -327,7 +327,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
if (tool === 'eraser') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (selectedEntity.renderer.bufferState) {
|
||||
if (selectedEntity.renderer.bufferState && selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.renderer.setBuffer({
|
||||
@@ -378,7 +378,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntity.renderer.bufferState) {
|
||||
if (selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
|
||||
@@ -397,7 +397,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
if (selectedEntity.renderer.bufferState) {
|
||||
if (selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.renderer.setBuffer({
|
||||
@@ -416,7 +416,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntity.renderer.bufferState) {
|
||||
if (selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.renderer.setBuffer({
|
||||
@@ -433,7 +433,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
if (selectedEntity.renderer.bufferState) {
|
||||
if (selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.renderer.setBuffer({
|
||||
@@ -447,7 +447,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'rect') {
|
||||
if (selectedEntity.renderer.bufferState) {
|
||||
if (selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.renderer.setBuffer({
|
||||
@@ -481,7 +481,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const tool = this.$tool.get();
|
||||
|
||||
if (tool === 'brush') {
|
||||
if (selectedEntity.renderer.bufferState?.type === 'brush_line') {
|
||||
if (selectedEntity.renderer.bufferState?.type === 'brush_line' && selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.renderer.clearBuffer();
|
||||
@@ -489,7 +489,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
if (selectedEntity.renderer.bufferState?.type === 'eraser_line') {
|
||||
if (selectedEntity.renderer.bufferState?.type === 'eraser_line' && selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.renderer.clearBuffer();
|
||||
@@ -497,7 +497,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'rect') {
|
||||
if (selectedEntity.renderer.bufferState?.type === 'rect') {
|
||||
if (selectedEntity.renderer.bufferState?.type === 'rect' && selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.renderer.clearBuffer();
|
||||
@@ -597,7 +597,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
this.$cursorPos.set(null);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (selectedEntity && selectedEntity.renderer.bufferState?.type !== 'rect') {
|
||||
if (selectedEntity && selectedEntity.renderer.bufferState?.type !== 'rect' && selectedEntity.renderer.hasBuffer()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Selector, Store } from '@reduxjs/toolkit';
|
||||
import type { CanvasEntityIdentifier, CanvasObjectState, Coordinate, Rect } from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
@@ -480,3 +481,22 @@ export const getLastPointOfLastLine = (
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type SubscriptionHandler<T> = (value: T, prevValue: T) => void;
|
||||
|
||||
export const createReduxSubscription = <T, U>(
|
||||
store: Store<T>,
|
||||
selector: Selector<T, U>,
|
||||
handler: SubscriptionHandler<U>
|
||||
) => {
|
||||
let prevValue: U = selector(store.getState());
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const value = selector(store.getState());
|
||||
if (value !== prevValue) {
|
||||
handler(value, prevValue);
|
||||
prevValue = value;
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user