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 2
- Rely on redux + reselect more - Remove all nanostores that simply "mirrored" redux state in favor of direct subscriptions to redux store - Add abstractions for creating redux subs and running selectors - Add `initialize` method to CanvasModuleBase, for post-instantiation tasks - Reduce local caching of state in modules to a minimum
This commit is contained in:
@@ -47,7 +47,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
|
||||
} else {
|
||||
canvasManager.filter.$config.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
|
||||
}
|
||||
canvasManager.filter.initialize(entityIdentifier);
|
||||
canvasManager.filter.startFilter(entityIdentifier);
|
||||
canvasManager.filter.previewFilter();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectCanvasSettingsSlice,
|
||||
settingsDynamicGridToggled,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectDynamicGrid, settingsDynamicGridToggled } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||
|
||||
export const CanvasSettingsDynamicGridSwitch = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { $socket } from 'app/hooks/useSocketIO';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
@@ -9,7 +8,7 @@ import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
@@ -47,9 +46,6 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null)
|
||||
}, [dpr]);
|
||||
};
|
||||
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
|
||||
|
||||
export const StageComponent = memo(() => {
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
canvasManager.filter.initialize(entityIdentifier);
|
||||
canvasManager.filter.startFilter(entityIdentifier);
|
||||
}, [canvasManager.filter, entityIdentifier]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useIsTransforming = () => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const transformingEntity = useStore(canvasManager.stateApi.$transformingAdapter);
|
||||
const isTransforming = useMemo(() => {
|
||||
return Boolean(transformingEntity);
|
||||
}, [transformingEntity]);
|
||||
const isTransforming = useStore(canvasManager.stateApi.$isTranforming);
|
||||
return isTransforming;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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 { createReduxSubscription, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectDynamicGrid } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
@@ -30,8 +29,6 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
dynamicGrid: boolean;
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
config: CanvasBackgroundModuleConfig = DEFAULT_CONFIG;
|
||||
|
||||
@@ -65,21 +62,24 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
||||
*/
|
||||
this.subscriptions.add(this.manager.stage.$stageAttrs.listen(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();
|
||||
})
|
||||
);
|
||||
/**
|
||||
* The background grid should be rendered when the dynamic grid setting changes.
|
||||
*/
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectDynamicGrid, this.render));
|
||||
}
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.render();
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the background grid.
|
||||
*/
|
||||
render = () => {
|
||||
if (!this.dynamicGrid) {
|
||||
const dynamicGrid = this.manager.stateApi.runSelector(selectDynamicGrid);
|
||||
|
||||
if (!dynamicGrid) {
|
||||
this.konva.layer.visible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,10 +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 { 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 { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectBbox } from 'features/controlLayers/store/selectors';
|
||||
import type { Coordinate, Rect } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
@@ -52,9 +51,7 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
* Buffer to store the last aspect ratio of the bbox. When the users holds shift while transforming the bbox, this is
|
||||
* used to lock the aspect ratio.
|
||||
*/
|
||||
$aspectRatioBuffer = atom(0);
|
||||
|
||||
state: CanvasState['bbox'];
|
||||
$aspectRatioBuffer = atom(1);
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
super();
|
||||
@@ -110,25 +107,25 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
this.subscriptions.add(this.manager.tool.$tool.listen(this.render));
|
||||
|
||||
// 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();
|
||||
})
|
||||
);
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectBbox, this.render));
|
||||
}
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
// We need to retain a copy of the bbox state because
|
||||
const { width, height } = this.manager.stateApi.runSelector(selectBbox).rect;
|
||||
// Update the aspect ratio buffer with the initial aspect ratio
|
||||
this.$aspectRatioBuffer.set(width / height);
|
||||
this.render();
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the bbox. The bbox is only visible when the tool is set to 'bbox'.
|
||||
*/
|
||||
render = () => {
|
||||
this.log.trace('Rendering');
|
||||
|
||||
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
|
||||
const { x, y, width, height } = this.manager.stateApi.runSelector(selectBbox).rect;
|
||||
const tool = this.manager.tool.$tool.get();
|
||||
|
||||
this.konva.group.visible(true);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer';
|
||||
import type { 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 { getIsHiddenSelector, selectCanvasSlice, 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';
|
||||
@@ -23,16 +24,6 @@ export abstract class CanvasEntityAdapterBase<
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -43,6 +34,38 @@ export abstract class CanvasEntityAdapterBase<
|
||||
*/
|
||||
abstract renderer: CanvasEntityObjectRenderer;
|
||||
|
||||
/**
|
||||
* Synchronizes 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.
|
||||
*
|
||||
* If `state` is undefined, the entity was just deleted and the adapter should destroy itself.
|
||||
*
|
||||
* If `prevState` is undefined, this is the first time the entity is being synced.
|
||||
*/
|
||||
abstract sync: (state: T | undefined, prevState: T | undefined) => 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;
|
||||
|
||||
/**
|
||||
* 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 entity's state.
|
||||
*/
|
||||
@@ -75,40 +98,36 @@ 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;
|
||||
// We must have the entity state on creation.
|
||||
const state = this.manager.stateApi.runSelector(this.selectState);
|
||||
assert(state !== undefined, 'Missing entity state on creation');
|
||||
this.state = state;
|
||||
|
||||
// When the hidden flag is updated, we need to update the entity's visibility and transformer interaction state,
|
||||
// which will show/hide the entity's selection outline
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.createStoreSubscription(getIsHiddenSelector(this.entityIdentifier.type), () => {
|
||||
this.syncOpacity();
|
||||
this.transformer.syncInteractionState();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest snapshot of the entity's state from the store. If the entity does not exist, returns undefined.
|
||||
* A redux selector that selects the entity's state from the canvas slice.
|
||||
*/
|
||||
getSnapshot = (): T | undefined => {
|
||||
return selectEntity(this.manager.stateApi.getCanvasState(), this.entityIdentifier) as T | undefined;
|
||||
selectState = createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) => selectEntity(canvas, this.entityIdentifier) as T | undefined
|
||||
);
|
||||
|
||||
initialize = async () => {
|
||||
this.log.debug('Initializing module');
|
||||
await this.sync(this.manager.stateApi.runSelector(this.selectState), undefined);
|
||||
this.transformer.initialize();
|
||||
await this.renderer.initialize();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -116,18 +135,18 @@ export abstract class CanvasEntityAdapterBase<
|
||||
this.log.trace('Updating visibility');
|
||||
this.konva.layer.visible(this.state.isEnabled);
|
||||
this.renderer.syncCache(this.state.isEnabled);
|
||||
this.transformer.syncInteractionState();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
});
|
||||
syncObjects = async () => {
|
||||
const didRender = await this.renderer.render();
|
||||
if (didRender) {
|
||||
// If the objects have changed, we need to recalculate the transformer's bounding box.
|
||||
this.transformer.requestRectCalculation();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,16 +15,14 @@ export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<Can
|
||||
|
||||
constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) {
|
||||
super(entityIdentifier, manager, CanvasEntityAdapterControlLayer.TYPE);
|
||||
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync));
|
||||
this.sync(true);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
|
||||
sync = (force?: boolean) => {
|
||||
const prevState = this.state;
|
||||
const state = this.getSnapshot();
|
||||
|
||||
sync = async (state: CanvasControlLayerState | undefined, prevState: CanvasControlLayerState | undefined) => {
|
||||
if (!state) {
|
||||
this.destroy();
|
||||
return;
|
||||
@@ -32,26 +30,26 @@ export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<Can
|
||||
|
||||
this.state = state;
|
||||
|
||||
if (!force && prevState === this.state) {
|
||||
if (prevState && prevState === this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force || this.state.isEnabled !== prevState.isEnabled) {
|
||||
if (!prevState || this.state.isEnabled !== prevState.isEnabled) {
|
||||
this.syncIsEnabled();
|
||||
}
|
||||
if (force || this.state.isLocked !== prevState.isLocked) {
|
||||
if (!prevState || this.state.isLocked !== prevState.isLocked) {
|
||||
this.syncIsLocked();
|
||||
}
|
||||
if (force || this.state.objects !== prevState.objects) {
|
||||
this.syncObjects();
|
||||
if (!prevState || this.state.objects !== prevState.objects) {
|
||||
await this.syncObjects();
|
||||
}
|
||||
if (force || this.state.position !== prevState.position) {
|
||||
if (!prevState || this.state.position !== prevState.position) {
|
||||
this.syncPosition();
|
||||
}
|
||||
if (force || this.state.opacity !== prevState.opacity) {
|
||||
if (!prevState || this.state.opacity !== prevState.opacity) {
|
||||
this.syncOpacity();
|
||||
}
|
||||
if (force || this.state.withTransparencyEffect !== prevState.withTransparencyEffect) {
|
||||
if (!prevState || this.state.withTransparencyEffect !== prevState.withTransparencyEffect) {
|
||||
this.renderer.updateTransparencyEffect();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,14 +19,10 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<Canv
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync));
|
||||
this.sync(true);
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
|
||||
sync = (force?: boolean) => {
|
||||
const prevState = this.state;
|
||||
const state = this.getSnapshot();
|
||||
|
||||
sync = async (state: CanvasInpaintMaskState | undefined, prevState: CanvasInpaintMaskState | undefined) => {
|
||||
if (!state) {
|
||||
this.destroy();
|
||||
return;
|
||||
@@ -34,29 +30,29 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<Canv
|
||||
|
||||
this.state = state;
|
||||
|
||||
if (!force && prevState === this.state) {
|
||||
if (prevState && prevState === this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force || this.state.isEnabled !== prevState.isEnabled) {
|
||||
if (!prevState || this.state.isEnabled !== prevState.isEnabled) {
|
||||
this.syncIsEnabled();
|
||||
}
|
||||
if (force || this.state.isLocked !== prevState.isLocked) {
|
||||
if (!prevState || this.state.isLocked !== prevState.isLocked) {
|
||||
this.syncIsLocked();
|
||||
}
|
||||
if (force || this.state.objects !== prevState.objects) {
|
||||
this.syncObjects();
|
||||
if (!prevState || this.state.objects !== prevState.objects) {
|
||||
await this.syncObjects();
|
||||
}
|
||||
if (force || this.state.position !== prevState.position) {
|
||||
if (!prevState || this.state.position !== prevState.position) {
|
||||
this.syncPosition();
|
||||
}
|
||||
if (force || this.state.opacity !== prevState.opacity) {
|
||||
if (!prevState || this.state.opacity !== prevState.opacity) {
|
||||
this.syncOpacity();
|
||||
}
|
||||
if (force || this.state.fill !== prevState.fill) {
|
||||
if (!prevState || this.state.fill !== prevState.fill) {
|
||||
this.syncCompositingRectFill();
|
||||
}
|
||||
if (force) {
|
||||
if (!prevState) {
|
||||
this.syncCompositingRectSize();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,16 +15,14 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<Canv
|
||||
|
||||
constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) {
|
||||
super(entityIdentifier, manager, CanvasEntityAdapterRasterLayer.TYPE);
|
||||
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync));
|
||||
this.sync(true);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
|
||||
sync = (force?: boolean) => {
|
||||
const prevState = this.state;
|
||||
const state = this.getSnapshot();
|
||||
|
||||
sync = async (state: CanvasRasterLayerState | undefined, prevState: CanvasRasterLayerState | undefined) => {
|
||||
if (!state) {
|
||||
this.destroy();
|
||||
return;
|
||||
@@ -32,23 +30,23 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<Canv
|
||||
|
||||
this.state = state;
|
||||
|
||||
if (!force && prevState === this.state) {
|
||||
if (prevState && prevState === this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force || this.state.isEnabled !== prevState.isEnabled) {
|
||||
if (!prevState || this.state.isEnabled !== prevState.isEnabled) {
|
||||
this.syncIsEnabled();
|
||||
}
|
||||
if (force || this.state.isLocked !== prevState.isLocked) {
|
||||
if (!prevState || this.state.isLocked !== prevState.isLocked) {
|
||||
this.syncIsLocked();
|
||||
}
|
||||
if (force || this.state.objects !== prevState.objects) {
|
||||
this.syncObjects();
|
||||
if (!prevState || this.state.objects !== prevState.objects) {
|
||||
await this.syncObjects();
|
||||
}
|
||||
if (force || this.state.position !== prevState.position) {
|
||||
if (!prevState || this.state.position !== prevState.position) {
|
||||
this.syncPosition();
|
||||
}
|
||||
if (force || this.state.opacity !== prevState.opacity) {
|
||||
if (!prevState || this.state.opacity !== prevState.opacity) {
|
||||
this.syncOpacity();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,14 +19,10 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.store.subscribe(this.sync));
|
||||
this.sync(true);
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
|
||||
sync = (force?: boolean) => {
|
||||
const prevState = this.state;
|
||||
const state = this.getSnapshot();
|
||||
|
||||
sync = async (state: CanvasRegionalGuidanceState | undefined, prevState: CanvasRegionalGuidanceState | undefined) => {
|
||||
if (!state) {
|
||||
this.destroy();
|
||||
return;
|
||||
@@ -34,29 +30,29 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
|
||||
|
||||
this.state = state;
|
||||
|
||||
if (!force && prevState === this.state) {
|
||||
if (prevState && prevState === this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force || this.state.isEnabled !== prevState.isEnabled) {
|
||||
if (!prevState || this.state.isEnabled !== prevState.isEnabled) {
|
||||
this.syncIsEnabled();
|
||||
}
|
||||
if (force || this.state.isLocked !== prevState.isLocked) {
|
||||
if (!prevState || this.state.isLocked !== prevState.isLocked) {
|
||||
this.syncIsLocked();
|
||||
}
|
||||
if (force || this.state.objects !== prevState.objects) {
|
||||
this.syncObjects();
|
||||
if (!prevState || this.state.objects !== prevState.objects) {
|
||||
await this.syncObjects();
|
||||
}
|
||||
if (force || this.state.position !== prevState.position) {
|
||||
if (!prevState || this.state.position !== prevState.position) {
|
||||
this.syncPosition();
|
||||
}
|
||||
if (force || this.state.opacity !== prevState.opacity) {
|
||||
if (!prevState || this.state.opacity !== prevState.opacity) {
|
||||
this.syncOpacity();
|
||||
}
|
||||
if (force || this.state.fill !== prevState.fill) {
|
||||
if (!prevState || this.state.fill !== prevState.fill) {
|
||||
this.syncCompositingRectFill();
|
||||
}
|
||||
if (force) {
|
||||
if (!prevState) {
|
||||
this.syncCompositingRectSize();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,7 +172,9 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
// to pan _before_ releasing the mouse button, which would cause the buffer to be lost if we didn't commit it.
|
||||
this.subscriptions.add(
|
||||
this.manager.tool.$tool.listen(() => {
|
||||
this.commitBuffer();
|
||||
if (this.hasBuffer()) {
|
||||
this.commitBuffer();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -190,6 +192,11 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
);
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
this.log.debug('Initializing module');
|
||||
await this.render();
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the entity's objects.
|
||||
* @returns A promise that resolves to a boolean, indicating if any of the objects were rendered.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { type CanvasState, getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
@@ -12,8 +13,6 @@ export class CanvasEntityRendererModule extends CanvasModuleBase {
|
||||
readonly parent: CanvasManager;
|
||||
readonly manager: CanvasManager;
|
||||
|
||||
private _state: CanvasState | null = null;
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
@@ -26,99 +25,61 @@ export class CanvasEntityRendererModule extends CanvasModuleBase {
|
||||
|
||||
this.log.debug('Creating module');
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.store.subscribe(this.render));
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSlice, this.render));
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const state = this.manager.stateApi.getCanvasState();
|
||||
|
||||
const prevState = this._state;
|
||||
this._state = state;
|
||||
|
||||
this.manager.stateApi.$settingsState.set(this.manager.stateApi.getSettings());
|
||||
this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier);
|
||||
this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentColor());
|
||||
|
||||
if (prevState === state) {
|
||||
// No changes to state - no need to render
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderRasterLayers(state, prevState);
|
||||
this.renderControlLayers(prevState, state);
|
||||
this.renderRegionalGuidance(prevState, state);
|
||||
this.renderInpaintMasks(state, prevState);
|
||||
this.arrangeEntities(state, prevState);
|
||||
this.manager.tool.syncCursorStyle();
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.render(this.manager.stateApi.runSelector(selectCanvasSlice), 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.syncOpacity();
|
||||
}
|
||||
}
|
||||
render = async (state: CanvasState, prevState: CanvasState | null) => {
|
||||
await this.createNewRasterLayers(state, prevState);
|
||||
await this.createNewControlLayers(state, prevState);
|
||||
await this.createNewRegionalGuidance(state, prevState);
|
||||
await this.createNewInpaintMasks(state, prevState);
|
||||
this.arrangeEntities(state, prevState);
|
||||
};
|
||||
|
||||
createNewRasterLayers = async (state: CanvasState, prevState: CanvasState | null) => {
|
||||
if (!prevState || state.rasterLayers.entities !== prevState.rasterLayers.entities) {
|
||||
for (const entityState of state.rasterLayers.entities) {
|
||||
if (!adapterMap.has(entityState.id)) {
|
||||
this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
if (!this.manager.adapters.rasterLayers.has(entityState.id)) {
|
||||
const adapter = this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
await adapter.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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.syncOpacity();
|
||||
}
|
||||
}
|
||||
|
||||
createNewControlLayers = async (state: CanvasState, prevState: CanvasState | null) => {
|
||||
if (!prevState || state.controlLayers.entities !== prevState.controlLayers.entities) {
|
||||
for (const entityState of state.controlLayers.entities) {
|
||||
if (!adapterMap.has(entityState.id)) {
|
||||
this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
if (!this.manager.adapters.controlLayers.has(entityState.id)) {
|
||||
const adapter = this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
await adapter.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderRegionalGuidance = (prevState: CanvasState | null, state: CanvasState) => {
|
||||
const adapterMap = this.manager.adapters.regionMasks;
|
||||
|
||||
if (!prevState || state.regions.isHidden !== prevState.regions.isHidden) {
|
||||
for (const adapter of adapterMap.values()) {
|
||||
adapter.syncOpacity();
|
||||
}
|
||||
}
|
||||
|
||||
createNewRegionalGuidance = async (state: CanvasState, prevState: CanvasState | null) => {
|
||||
if (!prevState || state.regions.entities !== prevState.regions.entities) {
|
||||
for (const entityState of state.regions.entities) {
|
||||
if (!adapterMap.has(entityState.id)) {
|
||||
this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
if (!this.manager.adapters.regionMasks.has(entityState.id)) {
|
||||
const adapter = this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
await adapter.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderInpaintMasks = (state: CanvasState, prevState: CanvasState | null) => {
|
||||
const adapterMap = this.manager.adapters.inpaintMasks;
|
||||
|
||||
if (!prevState || state.inpaintMasks.isHidden !== prevState.inpaintMasks.isHidden) {
|
||||
for (const adapter of adapterMap.values()) {
|
||||
adapter.syncOpacity();
|
||||
}
|
||||
}
|
||||
|
||||
createNewInpaintMasks = async (state: CanvasState, prevState: CanvasState | null) => {
|
||||
if (!prevState || state.inpaintMasks.entities !== prevState.inpaintMasks.entities) {
|
||||
for (const entityState of state.inpaintMasks.entities) {
|
||||
if (!adapterMap.has(entityState.id)) {
|
||||
this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
if (!this.manager.adapters.inpaintMasks.has(entityState.id)) {
|
||||
const adapter = this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
await adapter.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +94,7 @@ export class CanvasEntityRendererModule extends CanvasModuleBase {
|
||||
state.inpaintMasks.entities !== prevState.inpaintMasks.entities ||
|
||||
state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Arranging entities');
|
||||
this.log.trace('Arranging entities');
|
||||
|
||||
let zIndex = 0;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import type { Coordinate, Rect, RectWithRotation } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { GroupConfig } from 'konva/lib/Group';
|
||||
@@ -221,13 +222,20 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
|
||||
this.subscriptions.add(this.manager.tool.$tool.listen(this.syncInteractionState));
|
||||
|
||||
// When the selected entity changes, we need to update the transformer's interaction state.
|
||||
this.subscriptions.add(this.manager.stateApi.$selectedEntityIdentifier.listen(this.syncInteractionState));
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.createStoreSubscription(selectSelectedEntityIdentifier, this.syncInteractionState)
|
||||
);
|
||||
|
||||
this.parent.konva.layer.add(this.konva.outlineRect);
|
||||
this.parent.konva.layer.add(this.konva.proxyRect);
|
||||
this.parent.konva.layer.add(this.konva.transformer);
|
||||
}
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.syncInteractionState();
|
||||
};
|
||||
|
||||
anchorStyleFunc = (anchor: Konva.Rect): void => {
|
||||
// Give the rotater special styling
|
||||
if (anchor.hasName('rotater')) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export class CanvasFilterModule extends CanvasModuleBase {
|
||||
this.log.debug('Creating filter module');
|
||||
}
|
||||
|
||||
initialize = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
startFilter = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
this.log.trace('Initializing filter');
|
||||
const adapter = this.manager.getAdapter(entityIdentifier);
|
||||
if (!adapter) {
|
||||
|
||||
@@ -172,19 +172,23 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
];
|
||||
};
|
||||
|
||||
createAdapter = (entityIdentifier: CanvasEntityIdentifier): void => {
|
||||
createAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter => {
|
||||
if (isRasterLayerEntityIdentifier(entityIdentifier)) {
|
||||
const adapter = new CanvasEntityAdapterRasterLayer(entityIdentifier, this);
|
||||
this.adapters.rasterLayers.set(adapter.id, adapter);
|
||||
return adapter;
|
||||
} else if (isControlLayerEntityIdentifier(entityIdentifier)) {
|
||||
const adapter = new CanvasEntityAdapterControlLayer(entityIdentifier, this);
|
||||
this.adapters.controlLayers.set(adapter.id, adapter);
|
||||
return adapter;
|
||||
} else if (isRegionalGuidanceEntityIdentifier(entityIdentifier)) {
|
||||
const adapter = new CanvasEntityAdapterRegionalGuidance(entityIdentifier, this);
|
||||
this.adapters.regionMasks.set(adapter.id, adapter);
|
||||
return adapter;
|
||||
} else if (isInpaintMaskEntityIdentifier(entityIdentifier)) {
|
||||
const adapter = new CanvasEntityAdapterInpaintMask(entityIdentifier, this);
|
||||
this.adapters.inpaintMasks.set(adapter.id, adapter);
|
||||
return adapter;
|
||||
} else {
|
||||
assert(false, 'Unhandled entity type');
|
||||
}
|
||||
@@ -199,16 +203,29 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
this._isDebugging = false;
|
||||
}
|
||||
|
||||
getAllModules = (): CanvasModuleBase[] => {
|
||||
return [
|
||||
this.bbox,
|
||||
this.stagingArea,
|
||||
this.tool,
|
||||
this.progressImage,
|
||||
this.stateApi,
|
||||
this.background,
|
||||
this.filter,
|
||||
this.worker,
|
||||
this.entityRenderer,
|
||||
this.compositor,
|
||||
this.stage,
|
||||
];
|
||||
};
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing canvas manager module');
|
||||
this.log.debug('Initializing');
|
||||
|
||||
// These atoms require the canvas manager to be set up before we can provide their initial values
|
||||
this.stateApi.$transformingAdapter.set(null);
|
||||
this.stateApi.$settingsState.set(this.stateApi.getSettings());
|
||||
this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier);
|
||||
this.stateApi.$currentFill.set(this.stateApi.getCurrentColor());
|
||||
for (const canvasModule of this.getAllModules()) {
|
||||
canvasModule.initialize?.();
|
||||
}
|
||||
|
||||
this.stage.initialize();
|
||||
$canvasManager.set(this);
|
||||
};
|
||||
|
||||
@@ -219,19 +236,9 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
adapter.destroy();
|
||||
}
|
||||
|
||||
this.bbox.destroy();
|
||||
this.stagingArea.destroy();
|
||||
this.tool.destroy();
|
||||
this.progressImage.destroy();
|
||||
this.konva.previewLayer.destroy();
|
||||
|
||||
this.stateApi.destroy();
|
||||
this.background.destroy();
|
||||
this.filter.destroy();
|
||||
this.worker.destroy();
|
||||
this.entityRenderer.destroy();
|
||||
this.compositor.destroy();
|
||||
this.stage.destroy();
|
||||
for (const canvasModule of this.getAllModules()) {
|
||||
canvasModule.destroy();
|
||||
}
|
||||
|
||||
$canvasManager.set(null);
|
||||
};
|
||||
|
||||
@@ -55,6 +55,14 @@ export abstract class CanvasModuleBase {
|
||||
*/
|
||||
abstract readonly log: Logger;
|
||||
|
||||
/**
|
||||
* An optional method that initializes the module. This method is called after all modules have been created.
|
||||
*
|
||||
* Use this method to perform any setup that requires all modules to be created. For example, setting some initial
|
||||
* state or doing an initial render.
|
||||
*/
|
||||
initialize?: () => void = undefined;
|
||||
|
||||
/**
|
||||
* Returns a logging context object that includes relevant information about the module.
|
||||
* Canvas modules may override this method to include additional information in the logging context, but should
|
||||
|
||||
@@ -86,7 +86,7 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing stage');
|
||||
this.log.debug('Initializing module');
|
||||
this.konva.stage.container(this.container);
|
||||
const resizeObserver = new ResizeObserver(this.fitStageToContainer);
|
||||
resizeObserver.observe(this.container);
|
||||
|
||||
@@ -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 { createReduxSubscription, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { type CanvasSessionState, selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
@@ -16,8 +16,6 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasSessionState;
|
||||
|
||||
subscriptions: Set<() => void> = new Set();
|
||||
konva: { group: Konva.Group };
|
||||
image: CanvasObjectImage | null;
|
||||
@@ -40,22 +38,21 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
this.selectedImage = null;
|
||||
|
||||
this.subscriptions.add(this.$shouldShowStagedImage.listen(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();
|
||||
})
|
||||
);
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSessionSlice, this.render));
|
||||
}
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.render();
|
||||
};
|
||||
|
||||
render = async () => {
|
||||
this.log.trace('Rendering staging area');
|
||||
const stagingArea = this.manager.stateApi.runSelector(selectCanvasSessionSlice);
|
||||
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
|
||||
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
|
||||
|
||||
this.selectedImage = this.state.stagedImages[this.state.selectedStagedImageIndex] ?? null;
|
||||
this.selectedImage = stagingArea.stagedImages[stagingArea.selectedStagedImageIndex] ?? null;
|
||||
this.konva.group.position({ x, y });
|
||||
|
||||
if (this.selectedImage) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library';
|
||||
import type { AppStore } from 'app/store/store';
|
||||
import type { Selector } from '@reduxjs/toolkit';
|
||||
import type { AppStore, RootState } from 'app/store/store';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { SubscriptionHandler } from 'features/controlLayers/konva/util';
|
||||
import { createReduxSubscription, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
settingsBrushWidthChanged,
|
||||
settingsColorChanged,
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectAllRenderableEntities, selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEntityType,
|
||||
EntityBrushLineAddedPayload,
|
||||
EntityEraserLineAddedPayload,
|
||||
@@ -32,7 +32,6 @@ import type {
|
||||
RgbaColor,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
import type { WritableAtom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
@@ -68,6 +67,14 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
runSelector = <T>(selector: Selector<RootState, T>) => {
|
||||
return selector(this.store.getState());
|
||||
};
|
||||
|
||||
createStoreSubscription = <T>(selector: Selector<RootState, T>, handler: SubscriptionHandler<T>) => {
|
||||
return createReduxSubscription(this.store, selector, handler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the canvas slice.
|
||||
*
|
||||
@@ -310,21 +317,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
*/
|
||||
$isTranforming = computed(this.$transformingAdapter, (transformingAdapter) => Boolean(transformingAdapter));
|
||||
|
||||
/**
|
||||
* A nanostores atom, kept in sync with the redux store's settings state.
|
||||
*/
|
||||
$settingsState: WritableAtom<CanvasSettingsState> = atom();
|
||||
|
||||
/**
|
||||
* The current fill color, derived from the tool state and the selected entity.
|
||||
*/
|
||||
$currentFill: WritableAtom<RgbaColor> = atom();
|
||||
|
||||
/**
|
||||
* The currently selected entity's identifier, if an entity is selected.
|
||||
*/
|
||||
$selectedEntityIdentifier: WritableAtom<CanvasEntityIdentifier | null> = atom();
|
||||
|
||||
/**
|
||||
* The last canvas progress event. This is set in a global event listener. The staging area may set it to null when it
|
||||
* consumes the event.
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
isDistanceMoreThanMin,
|
||||
offsetCoord,
|
||||
} from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasInpaintMaskState,
|
||||
@@ -104,18 +106,8 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
this.konva.group.add(this.colorPickerToolPreview.konva.group);
|
||||
|
||||
this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render));
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.$settingsState.listen((settings, prevSettings) => {
|
||||
if (
|
||||
settings !== prevSettings ||
|
||||
settings.brushWidth !== prevSettings.brushWidth ||
|
||||
settings.eraserWidth !== prevSettings.eraserWidth ||
|
||||
settings.color !== prevSettings.color
|
||||
) {
|
||||
this.render();
|
||||
}
|
||||
})
|
||||
);
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSettingsSlice, this.render));
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSlice, this.syncCursorStyle));
|
||||
this.subscriptions.add(
|
||||
this.$tool.listen(() => {
|
||||
// On tool switch, reset mouse state
|
||||
@@ -129,6 +121,12 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
this.subscriptions.add(cleanupListeners);
|
||||
}
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.render();
|
||||
this.syncCursorStyle();
|
||||
};
|
||||
|
||||
setToolVisibility = (tool: Tool, isDrawable: boolean) => {
|
||||
this.brushToolPreview.setVisibility(isDrawable && tool === 'brush');
|
||||
this.eraserToolPreview.setVisibility(isDrawable && tool === 'eraser');
|
||||
|
||||
@@ -482,7 +482,7 @@ export const getLastPointOfLastLine = (
|
||||
return null;
|
||||
};
|
||||
|
||||
type SubscriptionHandler<T> = (value: T, prevValue: T) => void;
|
||||
export type SubscriptionHandler<T> = (value: T, prevValue: T) => void;
|
||||
|
||||
export const createReduxSubscription = <T, U>(
|
||||
store: Store<T>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
@@ -132,4 +132,12 @@ export const canvasSettingsPersistConfig: PersistConfig<CanvasSettingsState> = {
|
||||
migrate,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
export const selectCanvasSettingsSlice = (s: RootState) => s.canvasSettings;
|
||||
|
||||
export const selectDynamicGrid = createSelector(
|
||||
selectCanvasSettingsSlice,
|
||||
(canvasSettings) => canvasSettings.dynamicGrid
|
||||
);
|
||||
|
||||
export const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEntityState,
|
||||
CanvasEntityType,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
@@ -176,6 +177,8 @@ export function selectRegionalGuidanceIPAdapter(
|
||||
return entity.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
|
||||
}
|
||||
|
||||
export const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
|
||||
|
||||
export const selectSelectedEntityIdentifier = createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) => canvas.selectedEntityIdentifier
|
||||
@@ -215,3 +218,26 @@ export const selectSelectedEntityFill = createSelector(
|
||||
return entity.fill;
|
||||
}
|
||||
);
|
||||
|
||||
const selectRasterLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.isHidden);
|
||||
const selectControlLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.controlLayers.isHidden);
|
||||
const selectInpaintMasksIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.inpaintMasks.isHidden);
|
||||
const selectRegionalGuidanceIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.regions.isHidden);
|
||||
|
||||
/**
|
||||
* Returns the hidden selector for the given entity type.
|
||||
*/
|
||||
export const getIsHiddenSelector = (type: CanvasEntityType) => {
|
||||
switch (type) {
|
||||
case 'raster_layer':
|
||||
return selectRasterLayersIsHidden;
|
||||
case 'control_layer':
|
||||
return selectControlLayersIsHidden;
|
||||
case 'inpaint_mask':
|
||||
return selectInpaintMasksIsHidden;
|
||||
case 'regional_guidance':
|
||||
return selectRegionalGuidanceIsHidden;
|
||||
default:
|
||||
assert(false, 'Unhandled entity type');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user