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

View File

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

View File

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

View File

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

View File

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