feat(ui): move ephemeral tool state out of redux

This commit is contained in:
psychedelicious 2024-04-19 21:01:43 +10:00 committed by Kent Keirsey
parent 14c722c265
commit 9528287d56
5 changed files with 74 additions and 62 deletions

View File

@ -6,6 +6,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks';
import {
$cursorPosition,
$tool,
isRPLayer,
rpLayerBboxChanged,
rpLayerTranslated,
@ -35,6 +36,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
const height = useAppSelector((s) => s.generation.height);
const state = useAppSelector((s) => s.regionalPrompts.present);
const stage = useStore($stage);
const tool = useStore($tool);
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents();
const cursorPosition = useStore($cursorPosition);
const selectedLayerColor = useAppSelector(selectSelectedLayerColor);
@ -116,27 +118,28 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
}, [stage, width, height, wrapper]);
useLayoutEffect(() => {
log.trace('Rendering brush preview');
if (!stage) {
return;
}
renderBrushPreview(stage, state.tool, selectedLayerColor, cursorPosition, state.brushSize);
}, [stage, state.tool, cursorPosition, state.brushSize, selectedLayerColor]);
renderBrushPreview(stage, tool, selectedLayerColor, cursorPosition, state.brushSize);
}, [stage, tool, cursorPosition, state.brushSize, selectedLayerColor]);
useLayoutEffect(() => {
log.trace('Rendering layers');
if (!stage) {
return;
}
renderLayers(stage, state.layers, state.selectedLayer, state.promptLayerOpacity, state.tool, onLayerPosChanged);
}, [onLayerPosChanged, stage, state.layers, state.promptLayerOpacity, state.tool, state.selectedLayer]);
renderLayers(stage, state.layers, state.selectedLayer, state.promptLayerOpacity, tool, onLayerPosChanged);
}, [onLayerPosChanged, stage, state.layers, state.promptLayerOpacity, tool, state.selectedLayer]);
useLayoutEffect(() => {
log.trace('Rendering bbox');
if (!stage) {
return;
}
renderBbox(stage, state.tool, state.selectedLayer, onBboxChanged);
}, [dispatch, stage, state.tool, state.selectedLayer, onBboxChanged]);
renderBbox(stage, tool, state.selectedLayer, onBboxChanged);
}, [dispatch, stage, tool, state.selectedLayer, onBboxChanged]);
};
const $container = atom<HTMLDivElement | null>(null);

View File

@ -1,21 +1,20 @@
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { toolChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useStore } from '@nanostores/react';
import { $tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react';
import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold } from 'react-icons/pi';
export const ToolChooser: React.FC = () => {
const tool = useAppSelector((s) => s.regionalPrompts.present.tool);
const dispatch = useAppDispatch();
const tool = useStore($tool);
const setToolToBrush = useCallback(() => {
dispatch(toolChanged('brush'));
}, [dispatch]);
$tool.set('brush');
}, []);
const setToolToEraser = useCallback(() => {
dispatch(toolChanged('eraser'));
}, [dispatch]);
$tool.set('eraser');
}, []);
const setToolToMove = useCallback(() => {
dispatch(toolChanged('move'));
}, [dispatch]);
$tool.set('move');
}, []);
return (
<ButtonGroup isAttached>

View File

@ -1,10 +1,11 @@
import { getStore } from 'app/store/nanostores/store';
import { useAppDispatch } from 'app/store/storeHooks';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import {
$cursorPosition,
$isMouseDown,
$isMouseOver,
$tool,
rpLayerLineAdded,
rpLayerPointsAdded,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
@ -12,8 +13,6 @@ import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import { useCallback } from 'react';
const getTool = () => getStore().getState().regionalPrompts.present.tool;
const getIsFocused = (stage: Konva.Stage) => {
return stage.container().contains(document.activeElement);
};
@ -29,6 +28,8 @@ const syncCursorPos = (stage: Konva.Stage) => {
export const useMouseEvents = () => {
const dispatch = useAppDispatch();
const selectedLayer = useAppSelector((s) => s.regionalPrompts.present.selectedLayer);
const tool = useStore($tool);
const onMouseDown = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
@ -41,12 +42,15 @@ export const useMouseEvents = () => {
return;
}
$isMouseDown.set(true);
const tool = getTool();
if (!selectedLayer) {
return;
}
// const tool = getTool();
if (tool === 'brush' || tool === 'eraser') {
dispatch(rpLayerLineAdded([pos.x, pos.y, pos.x, pos.y]));
dispatch(rpLayerLineAdded({ layerId: selectedLayer, points: [pos.x, pos.y, pos.x, pos.y], tool }));
}
},
[dispatch]
[dispatch, selectedLayer, tool]
);
const onMouseUp = useCallback(
@ -55,12 +59,12 @@ export const useMouseEvents = () => {
if (!stage) {
return;
}
const tool = getTool();
// const tool = getTool();
if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) {
$isMouseDown.set(false);
}
},
[]
[tool]
);
const onMouseMove = useCallback(
@ -70,15 +74,15 @@ export const useMouseEvents = () => {
return;
}
const pos = syncCursorPos(stage);
if (!pos) {
if (!pos || !selectedLayer) {
return;
}
const tool = getTool();
// const tool = getTool();
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
dispatch(rpLayerPointsAdded([pos.x, pos.y]));
dispatch(rpLayerPointsAdded({ layerId: selectedLayer, point: [pos.x, pos.y] }));
}
},
[dispatch]
[dispatch, selectedLayer, tool]
);
const onMouseLeave = useCallback((e: KonvaEventObject<MouseEvent | TouchEvent>) => {
@ -109,13 +113,15 @@ export const useMouseEvents = () => {
$isMouseDown.set(false);
} else {
$isMouseDown.set(true);
const tool = getTool();
if (!selectedLayer) {
return;
}
if (tool === 'brush' || tool === 'eraser') {
dispatch(rpLayerLineAdded([pos.x, pos.y, pos.x, pos.y]));
dispatch(rpLayerLineAdded({ layerId: selectedLayer, points: [pos.x, pos.y, pos.x, pos.y], tool }));
}
}
},
[dispatch]
[dispatch, selectedLayer, tool]
);
return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave };

View File

@ -10,7 +10,9 @@ import type { UndoableOptions } from 'redux-undo';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
export type Tool = 'brush' | 'eraser' | 'move';
export type DrawingTool = 'brush' | 'eraser';
export type RPTool = DrawingTool | 'move';
type LayerObjectBase = {
id: string;
@ -27,7 +29,7 @@ type ImageObject = LayerObjectBase & {
type LineObject = LayerObjectBase & {
kind: 'line';
tool: Tool;
tool: DrawingTool;
strokeWidth: number;
points: number[];
};
@ -63,7 +65,6 @@ export type Layer = RegionalPromptLayer;
type RegionalPromptsState = {
_version: 1;
tool: Tool;
selectedLayer: string | null;
layers: Layer[];
brushSize: number;
@ -73,7 +74,6 @@ type RegionalPromptsState = {
export const initialRegionalPromptsState: RegionalPromptsState = {
_version: 1,
tool: 'brush',
selectedLayer: null,
brushSize: 40,
layers: [],
@ -197,34 +197,45 @@ export const regionalPromptsSlice = createSlice({
}
},
rpLayerLineAdded: {
reducer: (state, action: PayloadAction<[number, number, number, number], string, { uuid: string }>) => {
const layer = state.layers.find((l) => l.id === state.selectedLayer);
reducer: (
state,
action: PayloadAction<
{ layerId: string; points: [number, number, number, number]; tool: DrawingTool },
string,
{ uuid: string }
>
) => {
const { layerId, points, tool } = action.payload;
const layer = state.layers.find((l) => l.id === layerId);
if (isRPLayer(layer)) {
const lineId = getRPLayerLineId(layer.id, action.meta.uuid);
layer.objects.push({
kind: 'line',
tool: state.tool,
tool: tool,
id: lineId,
points: [
action.payload[0] - layer.x,
action.payload[1] - layer.y,
action.payload[2] - layer.x,
action.payload[3] - layer.y,
],
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
strokeWidth: state.brushSize,
});
}
},
prepare: (payload: [number, number, number, number]) => ({ payload, meta: { uuid: uuidv4() } }),
prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({
payload,
meta: { uuid: uuidv4() },
}),
},
rpLayerPointsAdded: (state, action: PayloadAction<[number, number]>) => {
const layer = state.layers.find((l) => l.id === state.selectedLayer);
rpLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => {
const { layerId, point } = action.payload;
const layer = state.layers.find((l) => l.id === layerId);
if (isRPLayer(layer)) {
const lastLine = layer.objects.findLast(isLine);
if (!lastLine) {
return;
}
lastLine.points.push(action.payload[0] - layer.x, action.payload[1] - layer.y);
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
}
},
rpLayerAutoNegativeChanged: (
@ -242,9 +253,6 @@ export const regionalPromptsSlice = createSlice({
brushSizeChanged: (state, action: PayloadAction<number>) => {
state.brushSize = action.payload;
},
toolChanged: (state, action: PayloadAction<Tool>) => {
state.tool = action.payload;
},
promptLayerOpacityChanged: (state, action: PayloadAction<number>) => {
state.promptLayerOpacity = action.payload;
},
@ -304,7 +312,6 @@ export const {
isEnabledChanged,
brushSizeChanged,
promptLayerOpacityChanged,
toolChanged,
} = regionalPromptsSlice.actions;
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
@ -316,6 +323,7 @@ const migrateRegionalPromptsState = (state: any): any => {
export const $isMouseDown = atom(false);
export const $isMouseOver = atom(false);
export const $tool = atom<RPTool>('brush');
export const $cursorPosition = atom<Vector2d | null>(null);
// IDs for singleton layers and objects
@ -394,10 +402,6 @@ export const regionalPromptsUndoableConfig: UndoableOptions<RegionalPromptsState
if (rpLayerBboxChanged.match(action)) {
return false;
}
// We don't want to record tool changes in the undo history
if (toolChanged.match(action)) {
return false;
}
return true;
},
};

View File

@ -1,6 +1,6 @@
import { rgbColorToString } from 'features/canvas/util/colorToString';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import type { Layer, RegionalPromptLayer, Tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
import type { Layer, RegionalPromptLayer, RPTool } from 'features/regionalPrompts/store/regionalPromptsSlice';
import {
BRUSH_PREVIEW_BORDER_INNER_ID,
BRUSH_PREVIEW_BORDER_OUTER_ID,
@ -36,7 +36,7 @@ const mapId = (object: { id: string }) => object.id;
*/
export const renderBrushPreview = (
stage: Konva.Stage,
tool: Tool,
tool: RPTool,
color: RgbColor | null,
cursorPos: Vector2d | null,
brushSize: number
@ -130,7 +130,7 @@ const renderRPLayer = (
rpLayer: RegionalPromptLayer,
rpLayerIndex: number,
selectedLayerId: string | null,
tool: Tool,
tool: RPTool,
layerOpacity: number,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
) => {
@ -278,7 +278,7 @@ export const renderLayers = (
reduxLayers: Layer[],
selectedLayerId: string | null,
layerOpacity: number,
tool: Tool,
tool: RPTool,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
) => {
const reduxLayerIds = reduxLayers.map(mapId);
@ -312,7 +312,7 @@ const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
*/
export const renderBbox = (
stage: Konva.Stage,
tool: Tool,
tool: RPTool,
selectedLayerId: string | null,
onBboxChanged: (layerId: string, bbox: IRect) => void
) => {