tidy(ui): merge tool slice, sendToCanvas into settings slice

This commit is contained in:
psychedelicious
2024-09-03 18:29:14 +10:00
parent 1fdb702557
commit 1349e73a1a
23 changed files with 196 additions and 202 deletions

View File

@@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
let didStartStaging = false;
if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) {
if (!state.canvasSession.isStaging && state.canvasSettings.sendToCanvas) {
dispatch(sessionStartedStaging());
didStartStaging = true;
}
@@ -70,7 +70,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const { g, noise, posCond } = buildGraphResult.value;
const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery';
const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
const prepareBatchResult = withResult(() =>
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)

View File

@@ -11,7 +11,6 @@ import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/contr
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
@@ -63,7 +62,6 @@ const allReducers = {
[upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.reducer,
[paramsSlice.name]: paramsSlice.reducer,
[toolSlice.name]: toolSlice.reducer,
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
@@ -109,7 +107,6 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
[paramsPersistConfig.name]: paramsPersistConfig,
[toolPersistConfig.name]: toolPersistConfig,
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
[canvasSessionPersistConfig.name]: canvasSessionPersistConfig,
[lorasPersistConfig.name]: lorasPersistConfig,

View File

@@ -1,7 +1,11 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IconSwitch } from 'common/components/IconSwitch';
import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice';
import {
selectCanvasSettingsSlice,
settingsSendToCanvasChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
@@ -32,20 +36,22 @@ const TooltipSendToCanvas = memo(() => {
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
const selectSendToCanvas = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.sendToCanvas);
export const CanvasSendToToggle = memo(() => {
const dispatch = useAppDispatch();
const isComposing = useAppSelector(selectIsComposing);
const sendToCanvas = useAppSelector(selectSendToCanvas);
const onChange = useCallback(
(isChecked: boolean) => {
dispatch(sessionSendToCanvasChanged(isChecked));
dispatch(settingsSendToCanvasChanged(isChecked));
},
[dispatch]
);
return (
<IconSwitch
isChecked={isComposing}
isChecked={sendToCanvas}
onChange={onChange}
iconUnchecked={<PiImageBold />}
tooltipUnchecked={<TooltipSendToGallery />}

View File

@@ -1,7 +1,7 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clipToBboxChanged, selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectCanvasSettingsSlice,settingsClipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,7 +13,7 @@ export const CanvasSettingsClipToBboxCheckbox = memo(() => {
const dispatch = useAppDispatch();
const clipToBbox = useAppSelector(selectClipToBbox);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
(e: ChangeEvent<HTMLInputElement>) => dispatch(settingsClipToBboxChanged(e.target.checked)),
[dispatch]
);
return (

View File

@@ -1,25 +1,30 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { invertScrollChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import { selectCanvasSettingsSlice, settingsInvertScrollForToolWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectInvertScroll = createSelector(selectToolSlice, (tool) => tool.invertScroll);
const selectInvertScrollForToolWidth = createSelector(
selectCanvasSettingsSlice,
(settings) => settings.invertScrollForToolWidth
);
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const invertScroll = useAppSelector(selectInvertScroll);
const invertScrollForToolWidth = useAppSelector(selectInvertScrollForToolWidth);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(settingsInvertScrollForToolWidthChanged(e.target.checked));
},
[dispatch]
);
return (
<FormControl w="full">
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
<Checkbox isChecked={invertScroll} onChange={onChange} />
<Checkbox isChecked={invertScrollForToolWidth} onChange={onChange} />
</FormControl>
);
});

View File

@@ -15,7 +15,7 @@ import {
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { brushWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import { selectCanvasSettingsSlice, settingsBrushWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
@@ -23,7 +23,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
const selectBrushWidth = createSelector(selectToolSlice, (tool) => tool.brush.width);
const selectBrushWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.brushWidth);
const formatPx = (v: number | string) => `${v} px`;
function mapSliderValueToRawValue(value: number) {
@@ -73,7 +73,7 @@ export const ToolBrushWidth = memo(() => {
const [localValue, setLocalValue] = useState(width);
const onChange = useCallback(
(v: number) => {
dispatch(brushWidthChanged(clamp(Math.round(v), 1, 600)));
dispatch(settingsBrushWidthChanged(clamp(Math.round(v), 1, 600)));
},
[dispatch]
);

View File

@@ -15,7 +15,7 @@ import {
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
import { eraserWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import { selectCanvasSettingsSlice, settingsEraserWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { clamp } from 'lodash-es';
import type { KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
@@ -23,7 +23,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
const selectEraserWidth = createSelector(selectToolSlice, (tool) => tool.eraser.width);
const selectEraserWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.eraserWidth);
const formatPx = (v: number | string) => `${v} px`;
function mapSliderValueToRawValue(value: number) {
@@ -73,7 +73,7 @@ export const ToolEraserWidth = memo(() => {
const [localValue, setLocalValue] = useState(width);
const onChange = useCallback(
(v: number) => {
dispatch(eraserWidthChanged(clamp(Math.round(v), 1, 600)));
dispatch(settingsEraserWidthChanged(clamp(Math.round(v), 1, 600)));
},
[dispatch]
);

View File

@@ -3,20 +3,20 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { fillChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
import { selectCanvasSettingsSlice, settingsColorChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectFill = createSelector(selectToolSlice, (tool) => tool.fill);
const selectColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.color);
export const ToolFillColorPicker = memo(() => {
export const ToolColorPicker = memo(() => {
const { t } = useTranslation();
const fill = useAppSelector(selectFill);
const fill = useAppSelector(selectColor);
const dispatch = useAppDispatch();
const onChange = useCallback(
(color: RgbaColor) => {
dispatch(fillChanged(color));
dispatch(settingsColorChanged(color));
},
[dispatch]
);
@@ -40,4 +40,4 @@ export const ToolFillColorPicker = memo(() => {
);
});
ToolFillColorPicker.displayName = 'ToolFillColorPicker';
ToolColorPicker.displayName = 'ToolFillColorPicker';

View File

@@ -2,7 +2,7 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
import { CanvasToolbarResetViewButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton';
import { CanvasToolbarSaveToGalleryButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarSaveToGalleryButton';
@@ -35,7 +35,7 @@ export const CanvasToolbar = memo(() => {
<CanvasToolbarScale />
<CanvasToolbarResetViewButton />
<Spacer />
<ToolFillColorPicker />
<ToolColorPicker />
<CanvasToolbarSaveToGalleryButton />
<CanvasSettingsPopover />
<ViewerToggle />

View File

@@ -94,10 +94,10 @@ export class CanvasBrushToolPreview extends CanvasModuleBase {
return;
}
const toolState = this.manager.stateApi.getToolState();
const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill();
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
const radius = toolState.brush.width / 2;
const settings = this.manager.stateApi.getSettings();
const brushPreviewFill = this.manager.stateApi.getBrushPreviewColor();
const alignedCursorPos = alignCoordForTool(cursorPos, settings.brushWidth);
const radius = settings.brushWidth / 2;
// The circle is scaled
this.konva.fillCircle.setAttrs({

View File

@@ -198,7 +198,7 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase {
return;
}
const toolState = this.manager.stateApi.getToolState();
const settings = this.manager.stateApi.getSettings();
const colorUnderCursor = this.parent.$colorUnderCursor.get();
const colorPickerInnerRadius = this.manager.stage.getScaledPixels(this.config.RING_INNER_RADIUS);
const colorPickerOuterRadius = this.manager.stage.getScaledPixels(this.config.RING_OUTER_RADIUS);
@@ -215,7 +215,7 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase {
this.konva.ringCurrentColor.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
fill: rgbColorToString(toolState.fill),
fill: rgbColorToString(settings.color),
innerRadius: colorPickerInnerRadius,
outerRadius: colorPickerOuterRadius,
});

View File

@@ -84,9 +84,9 @@ export class CanvasEraserToolPreview extends CanvasModuleBase {
return;
}
const toolState = this.manager.stateApi.getToolState();
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
const radius = toolState.eraser.width / 2;
const settings = this.manager.stateApi.getSettings();
const alignedCursorPos = alignCoordForTool(cursorPos, settings.eraserWidth);
const radius = settings.eraserWidth / 2;
// The circle is scaled
this.konva.cutoutCircle.setAttrs({

View File

@@ -136,9 +136,9 @@ export class CanvasManager extends CanvasModuleBase {
// These atoms require the canvas manager to be set up before we can provide their initial values
this.stateApi.$transformingAdapter.set(null);
this.stateApi.$toolState.set(this.stateApi.getToolState());
this.stateApi.$settingsState.set(this.stateApi.getSettings());
this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier);
this.stateApi.$currentFill.set(this.stateApi.getCurrentFill());
this.stateApi.$currentFill.set(this.stateApi.getCurrentColor());
this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity());
this.subscriptions.add(this.store.subscribe(this.renderer.render));

View File

@@ -55,10 +55,10 @@ export class CanvasRenderingModule extends CanvasModuleBase {
const prevState = this.state;
this.state = state;
this.manager.stateApi.$toolState.set(this.manager.stateApi.getToolState());
this.manager.stateApi.$settingsState.set(this.manager.stateApi.getSettings());
this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier);
this.manager.stateApi.$selectedEntity.set(this.manager.stateApi.getSelectedEntity());
this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill());
this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentColor());
if (prevState === state) {
// No changes to state - no need to render

View File

@@ -5,6 +5,13 @@ import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/Canva
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 {
settingsBrushWidthChanged,
settingsColorChanged,
settingsEraserWidthChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import {
bboxChanged,
entityBrushLineAdded,
@@ -15,12 +22,6 @@ import {
entityReset,
} from 'features/controlLayers/store/canvasSlice';
import { selectAllRenderableEntities, selectCanvasSlice } from 'features/controlLayers/store/selectors';
import {
brushWidthChanged,
eraserWidthChanged,
fillChanged,
type ToolState,
} from 'features/controlLayers/store/toolSlice';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
@@ -158,21 +159,21 @@ export class CanvasStateApiModule extends CanvasModuleBase {
* Sets the brush width, pushing state to redux.
*/
setBrushWidth = (width: number) => {
this.store.dispatch(brushWidthChanged(width));
this.store.dispatch(settingsBrushWidthChanged(width));
};
/**
* Sets the eraser width, pushing state to redux.
*/
setEraserWidth = (width: number) => {
this.store.dispatch(eraserWidthChanged(width));
this.store.dispatch(settingsEraserWidthChanged(width));
};
/**
* Sets the fill color, pushing state to redux.
* Sets the drawing color, pushing state to redux.
*/
setFill = (fill: RgbaColor) => {
return this.store.dispatch(fillChanged(fill));
setColor = (color: RgbaColor) => {
return this.store.dispatch(settingsColorChanged(color));
};
/**
@@ -193,13 +194,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
return this.getCanvasState().bbox;
};
/**
* Gets the tool state from redux.
*/
getToolState = () => {
return this.store.getState().tool;
};
/**
* Gets the canvas settings from redux.
*/
@@ -322,37 +316,36 @@ export class CanvasStateApiModule extends CanvasModuleBase {
};
/**
* Gets the current fill color. The fill color is determined by the tool state and the selected entity.
* Gets the current drawing color.
*
* The fill color is determined by the tool state, except when the selected entity is a regional guidance or inpaint
* mask. In that case, the fill color is always black.
* The color is determined by the tool state, except when the selected entity is a regional guidance or inpaint mask.
* In that case, the color is always black.
*
* Regional guidance and inpaint mask entities use a compositing rect to draw with their selected color and texture,
* so the fill color for lines and rects doesn't matter - it is never seen. The only requirement is that it is opaque.
* For consistency with conventional black and white mask images, we use black as the fill color for these entities.
* so the color for lines and rects doesn't matter - it is never seen. The only requirement is that it is opaque. For
* consistency with conventional black and white mask images, we use black as the color for these entities.
*/
getCurrentFill = () => {
let currentFill: RgbaColor = this.getToolState().fill;
getCurrentColor = () => {
let color: RgbaColor = this.getSettings().color;
const selectedEntity = this.getSelectedEntity();
if (selectedEntity) {
// These two entity types use a compositing rect for opacity. Their fill is always a solid color.
if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') {
currentFill = RGBA_BLACK;
color = RGBA_BLACK;
}
}
return currentFill;
return color;
};
/**
* Gets the brush preview fill color. The brush preview fill color is determined by the tool state and the selected
* entity.
* Gets the brush preview color. The brush preview color is determined by the tool state and the selected entity.
*
* The color is the tool state's fill color, except when the selected entity is a regional guidance or inpaint mask.
* The color is the tool state's color, except when the selected entity is a regional guidance or inpaint mask.
*
* These entities have their own fill color and texture, so the brush preview should use those instead of the tool
* state's fill color.
* These entities have their own color and texture, so the brush preview should use those instead of the tool state's
* color.
*/
getBrushPreviewFill = (): RgbaColor => {
getBrushPreviewColor = (): RgbaColor => {
const selectedEntity = this.getSelectedEntity();
if (selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'inpaint_mask') {
// TODO(psyche): If we move the brush preview's Konva nodes to the selected entity renderer, we can draw them
@@ -361,7 +354,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
// selected entity's fill color with 50% opacity.
return { ...selectedEntity.state.fill.color, a: 0.5 };
} else {
return this.getToolState().fill;
return this.getSettings().color;
}
};
@@ -376,9 +369,9 @@ export class CanvasStateApiModule extends CanvasModuleBase {
$isTranforming = computed(this.$transformingAdapter, (transformingAdapter) => Boolean(transformingAdapter));
/**
* A nanostores atom, kept in sync with the redux store's tool state.
* A nanostores atom, kept in sync with the redux store's settings state.
*/
$toolState: WritableAtom<ToolState> = atom();
$settingsState: WritableAtom<CanvasSettingsState> = atom();
/**
* The current fill color, derived from the tool state and the selected entity.

View File

@@ -112,12 +112,12 @@ export class CanvasToolModule extends CanvasModuleBase {
this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render));
this.subscriptions.add(
this.manager.stateApi.$toolState.listen((value, oldValue) => {
this.manager.stateApi.$settingsState.listen((settings, prevSettings) => {
if (
value !== oldValue ||
value.brush.width !== oldValue.brush.width ||
value.eraser.width !== oldValue.eraser.width ||
value.fill !== oldValue.fill
settings !== prevSettings ||
settings.brushWidth !== prevSettings.brushWidth ||
settings.eraserWidth !== prevSettings.eraserWidth ||
settings.color !== prevSettings.color
) {
this.render();
}
@@ -306,7 +306,7 @@ export class CanvasToolModule extends CanvasModuleBase {
const cursorPos = this.syncLastCursorPos();
try {
const isMouseDown = this.$isMouseDown.get();
const toolState = this.manager.stateApi.getToolState();
const settings = this.manager.stateApi.getSettings();
const tool = this.$tool.get();
const selectedEntity = this.manager.stateApi.getSelectedEntity();
@@ -321,13 +321,13 @@ export class CanvasToolModule extends CanvasModuleBase {
if (tool === 'brush') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
await selectedEntity.adapter.renderer.setBuffer({
id: getPrefixedId('brush_line'),
type: 'brush_line',
points: [alignedPoint.x, alignedPoint.y],
strokeWidth: toolState.brush.width,
color: this.manager.stateApi.getCurrentFill(),
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.getClip(selectedEntity.state),
});
this.$lastAddedPoint.set(alignedPoint);
@@ -336,7 +336,7 @@ export class CanvasToolModule extends CanvasModuleBase {
if (tool === 'eraser') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (selectedEntity.adapter.renderer.bufferState) {
selectedEntity.adapter.renderer.commitBuffer();
}
@@ -344,7 +344,7 @@ export class CanvasToolModule extends CanvasModuleBase {
id: getPrefixedId('eraser_line'),
type: 'eraser_line',
points: [alignedPoint.x, alignedPoint.y],
strokeWidth: toolState.eraser.width,
strokeWidth: settings.eraserWidth,
clip: this.getClip(selectedEntity.state),
});
this.$lastAddedPoint.set(alignedPoint);
@@ -361,12 +361,12 @@ export class CanvasToolModule extends CanvasModuleBase {
try {
const tool = this.$tool.get();
const toolState = this.manager.stateApi.getToolState();
const settings = this.manager.stateApi.getSettings();
if (tool === 'colorPicker') {
const color = this.getColorUnderCursor();
if (color) {
this.manager.stateApi.setFill({ ...toolState.fill, ...color });
this.manager.stateApi.setColor({ ...settings.color, ...color });
}
return;
}
@@ -382,7 +382,7 @@ export class CanvasToolModule extends CanvasModuleBase {
if (tool === 'brush') {
const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line');
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
if (selectedEntity.adapter.renderer.bufferState) {
@@ -399,8 +399,8 @@ export class CanvasToolModule extends CanvasModuleBase {
alignedPoint.x,
alignedPoint.y,
],
strokeWidth: toolState.brush.width,
color: this.manager.stateApi.getCurrentFill(),
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.getClip(selectedEntity.state),
});
} else {
@@ -411,8 +411,8 @@ export class CanvasToolModule extends CanvasModuleBase {
id: getPrefixedId('brush_line'),
type: 'brush_line',
points: [alignedPoint.x, alignedPoint.y],
strokeWidth: toolState.brush.width,
color: this.manager.stateApi.getCurrentFill(),
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.getClip(selectedEntity.state),
});
}
@@ -421,7 +421,7 @@ export class CanvasToolModule extends CanvasModuleBase {
if (tool === 'eraser') {
const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line');
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
if (selectedEntity.adapter.renderer.bufferState) {
@@ -437,7 +437,7 @@ export class CanvasToolModule extends CanvasModuleBase {
alignedPoint.x,
alignedPoint.y,
],
strokeWidth: toolState.eraser.width,
strokeWidth: settings.eraserWidth,
clip: this.getClip(selectedEntity.state),
});
} else {
@@ -448,7 +448,7 @@ export class CanvasToolModule extends CanvasModuleBase {
id: getPrefixedId('eraser_line'),
type: 'eraser_line',
points: [alignedPoint.x, alignedPoint.y],
strokeWidth: toolState.eraser.width,
strokeWidth: settings.eraserWidth,
clip: this.getClip(selectedEntity.state),
});
}
@@ -463,7 +463,7 @@ export class CanvasToolModule extends CanvasModuleBase {
id: getPrefixedId('rect'),
type: 'rect',
rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 },
color: this.manager.stateApi.getCurrentFill(),
color: this.manager.stateApi.getCurrentColor(),
});
}
} finally {
@@ -542,17 +542,17 @@ export class CanvasToolModule extends CanvasModuleBase {
return;
}
const toolState = this.manager.stateApi.getToolState();
const settings = this.manager.stateApi.getSettings();
if (tool === 'brush' && bufferState.type === 'brush_line') {
const lastPoint = getLastPointOfLine(bufferState.points);
const minDistance = toolState.brush.width * this.config.BRUSH_SPACING_TARGET_SCALE;
const minDistance = settings.brushWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
return;
}
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) {
// Do not add duplicate points
@@ -564,13 +564,13 @@ export class CanvasToolModule extends CanvasModuleBase {
this.$lastAddedPoint.set(alignedPoint);
} else if (tool === 'eraser' && bufferState.type === 'eraser_line') {
const lastPoint = getLastPointOfLine(bufferState.points);
const minDistance = toolState.eraser.width * this.config.BRUSH_SPACING_TARGET_SCALE;
const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
return;
}
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) {
// Do not add duplicate points
@@ -613,20 +613,20 @@ export class CanvasToolModule extends CanvasModuleBase {
return;
}
const toolState = this.manager.stateApi.getToolState();
const settings = this.manager.stateApi.getSettings();
const tool = this.$tool.get();
let delta = e.evt.deltaY;
if (toolState.invertScroll) {
if (settings.invertScrollForToolWidth) {
delta = -delta;
}
// Holding ctrl or meta while scrolling changes the brush size
if (tool === 'brush') {
this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(toolState.brush.width, delta));
this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(settings.brushWidth, delta));
} else if (tool === 'eraser') {
this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(toolState.eraser.width, delta));
this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(settings.eraserWidth, delta));
}
this.render();

View File

@@ -4,14 +4,12 @@ import { canvasSlice } from 'features/controlLayers/store/canvasSlice';
import type { StagingAreaImage } from 'features/controlLayers/store/types';
export type CanvasSessionState = {
sendToCanvas: boolean;
isStaging: boolean;
stagedImages: StagingAreaImage[];
selectedStagedImageIndex: number;
};
const initialState: CanvasSessionState = {
sendToCanvas: false,
isStaging: false,
stagedImages: [],
selectedStagedImageIndex: 0,
@@ -51,9 +49,6 @@ export const canvasSessionSlice = createSlice({
state.stagedImages = [];
state.selectedStagedImageIndex = 0;
},
sessionSendToCanvasChanged: (state, action: PayloadAction<boolean>) => {
state.sendToCanvas = action.payload;
},
},
});
@@ -64,7 +59,6 @@ export const {
sessionStagingAreaReset,
sessionNextStagedImageSelected,
sessionPrevStagedImageSelected,
sessionSendToCanvasChanged,
} = canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -85,7 +79,3 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>(
export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession;
export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging);
export const selectIsComposing = createSelector(
selectCanvasSessionSlice,
(canvasSession) => canvasSession.sendToCanvas
);

View File

@@ -1,32 +1,71 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import type { RgbaColor } from 'features/controlLayers/store/types';
export type CanvasSettingsState = {
imageSmoothing: boolean;
/**
* Whether to show HUD (Heads-Up Display) on the canvas.
*/
showHUD: boolean;
/**
* Whether to automatically save canvas generations to the gallery. If in Save to Gallery mode, this setting will be
* ignored, and all generations will be saved.
*/
autoSave: boolean;
preserveMaskedArea: boolean;
cropToBboxOnSave: boolean;
/**
* Whether to clip lines and shapes to the generation bounding box. If disabled, lines and shapes will be clipped to
* the canvas bounds.
*/
clipToBbox: boolean;
/**
* Whether to show a dynamic grid on the canvas. If disabled, a checkerboard pattern will be shown instead.
*/
dynamicGrid: boolean;
/**
* Whether to invert the scroll direction when adjusting the brush or eraser width with the scroll wheel.
*/
invertScrollForToolWidth: boolean;
/**
* The width of the brush tool.
*/
brushWidth: number;
/**
* The width of the eraser tool.
*/
eraserWidth: number;
/**
* The color to use when drawing lines or filling shapes.
*/
color: RgbaColor;
/**
* Whether to send generated images to canvas staging area. When disabled, generated images will be sent directly to
* the gallery.
*/
sendToCanvas: boolean;
// TODO(psyche): These are copied from old canvas state, need to be implemented
// imageSmoothing: boolean;
// preserveMaskedArea: boolean;
// cropToBboxOnSave: boolean;
};
const initialState: CanvasSettingsState = {
// TODO(psyche): These are copied from old canvas state, need to be implemented
autoSave: false,
imageSmoothing: true,
preserveMaskedArea: false,
showHUD: true,
clipToBbox: false,
cropToBboxOnSave: false,
dynamicGrid: false,
brushWidth: 50,
eraserWidth: 50,
invertScrollForToolWidth: false,
color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
sendToCanvas: false,
};
export const canvasSettingsSlice = createSlice({
name: 'canvasSettings',
initialState,
reducers: {
clipToBboxChanged: (state, action: PayloadAction<boolean>) => {
settingsClipToBboxChanged: (state, action: PayloadAction<boolean>) => {
state.clipToBbox = action.payload;
},
settingsDynamicGridToggled: (state) => {
@@ -38,11 +77,35 @@ export const canvasSettingsSlice = createSlice({
settingsShowHUDToggled: (state) => {
state.showHUD = !state.showHUD;
},
settingsBrushWidthChanged: (state, action: PayloadAction<number>) => {
state.brushWidth = Math.round(action.payload);
},
settingsEraserWidthChanged: (state, action: PayloadAction<number>) => {
state.eraserWidth = Math.round(action.payload);
},
settingsColorChanged: (state, action: PayloadAction<RgbaColor>) => {
state.color = action.payload;
},
settingsInvertScrollForToolWidthChanged: (state, action: PayloadAction<boolean>) => {
state.invertScrollForToolWidth = action.payload;
},
settingsSendToCanvasChanged: (state, action: PayloadAction<boolean>) => {
state.sendToCanvas = action.payload;
},
},
});
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled, settingsShowHUDToggled } =
canvasSettingsSlice.actions;
export const {
settingsClipToBboxChanged,
settingsAutoSaveToggled,
settingsDynamicGridToggled,
settingsShowHUDToggled,
settingsBrushWidthChanged,
settingsEraserWidthChanged,
settingsColorChanged,
settingsInvertScrollForToolWidthChanged,
settingsSendToCanvasChanged,
} = canvasSettingsSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {

View File

@@ -1,56 +0,0 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import type { RgbaColor } from 'features/controlLayers/store/types';
export type ToolState = {
invertScroll: boolean;
brush: { width: number };
eraser: { width: number };
fill: RgbaColor;
};
const initialState: ToolState = {
invertScroll: false,
fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
brush: {
width: 50,
},
eraser: {
width: 50,
},
};
export const toolSlice = createSlice({
name: 'tool',
initialState,
reducers: {
brushWidthChanged: (state, action: PayloadAction<number>) => {
state.brush.width = Math.round(action.payload);
},
eraserWidthChanged: (state, action: PayloadAction<number>) => {
state.eraser.width = Math.round(action.payload);
},
fillChanged: (state, action: PayloadAction<RgbaColor>) => {
state.fill = action.payload;
},
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
state.invertScroll = action.payload;
},
},
});
export const { brushWidthChanged, eraserWidthChanged, fillChanged, invertScrollChanged } = toolSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const toolPersistConfig: PersistConfig<ToolState> = {
name: toolSlice.name,
initialState,
migrate,
persistDenylist: [],
};
export const selectToolSlice = (state: RootState) => state.tool;

View File

@@ -1,7 +1,7 @@
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { Dimensions } from 'features/controlLayers/store/types';
@@ -25,11 +25,11 @@ export const addInpaint = async (
denoise.denoising_start = denoising_start;
const params = selectParamsSlice(state);
const canvasSession = selectCanvasSessionSlice(state);
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
const { sendToCanvas: isComposing } = canvasSession;
const { sendToCanvas } = canvasSettings;
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
@@ -99,7 +99,7 @@ export const addInpaint = async (
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
if (!isComposing) {
if (!sendToCanvas) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}
@@ -143,7 +143,7 @@ export const addInpaint = async (
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
if (!isComposing) {
if (!sendToCanvas) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}

View File

@@ -1,7 +1,7 @@
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { Dimensions } from 'features/controlLayers/store/types';
@@ -26,11 +26,11 @@ export const addOutpaint = async (
denoise.denoising_start = denoising_start;
const params = selectParamsSlice(state);
const canvasSession = selectCanvasSessionSlice(state);
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
const { sendToCanvas: isComposing } = canvasSession;
const { sendToCanvas } = canvasSettings;
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
@@ -123,7 +123,7 @@ export const addOutpaint = async (
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
if (!isComposing) {
if (!sendToCanvas) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}
@@ -173,7 +173,7 @@ export const addOutpaint = async (
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
if (!isComposing) {
if (!sendToCanvas) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}

View File

@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
@@ -36,7 +35,6 @@ export const buildSD1Graph = async (
log.debug({ generationMode }, 'Building SD1/SD2 graph');
const params = selectParamsSlice(state);
const canvasSession = selectCanvasSessionSlice(state);
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
@@ -282,7 +280,7 @@ export const buildSD1Graph = async (
canvasOutput = addWatermarker(g, canvasOutput);
}
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
const shouldSaveToGallery = !canvasSettings.sendToCanvas || canvasSettings.autoSave;
g.updateNode(canvasOutput, {
id: getPrefixedId('canvas_output'),

View File

@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
@@ -36,7 +35,6 @@ export const buildSDXLGraph = async (
log.debug({ generationMode }, 'Building SDXL graph');
const params = selectParamsSlice(state);
const canvasSession = selectCanvasSessionSlice(state);
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
@@ -285,7 +283,7 @@ export const buildSDXLGraph = async (
canvasOutput = addWatermarker(g, canvasOutput);
}
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
const shouldSaveToGallery = !canvasSettings.sendToCanvas || canvasSettings.autoSave;
g.updateNode(canvasOutput, {
id: getPrefixedId('canvas_output'),