feat(ui): wip regional prompting UI

- Add eraser tool, applies per layer
This commit is contained in:
psychedelicious 2024-04-09 19:25:45 +10:00 committed by Kent Keirsey
parent 822dfa77fc
commit 52ba4966c9
6 changed files with 66 additions and 10 deletions

View File

@ -11,6 +11,7 @@ import { createStore } from '../src/app/store/store';
// @ts-ignore // @ts-ignore
import translationEN from '../public/locales/en.json'; import translationEN from '../public/locales/en.json';
import { ReduxInit } from './ReduxInit'; import { ReduxInit } from './ReduxInit';
import { $store } from 'app/store/nanostores/store';
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
lng: 'en', lng: 'en',
@ -25,6 +26,7 @@ i18n.use(initReactI18next).init({
}); });
const store = createStore(undefined, false); const store = createStore(undefined, false);
$store.set(store)
$baseUrl.set('http://localhost:9090'); $baseUrl.set('http://localhost:9090');
const preview: Preview = { const preview: Preview = {

View File

@ -24,6 +24,7 @@ export const LineComponent = ({ line, color }: Props) => {
lineJoin="round" lineJoin="round"
shadowForStrokeEnabled={false} shadowForStrokeEnabled={false}
listening={false} listening={false}
globalCompositeOperation={line.tool === 'brush' ? 'source-over' : 'destination-out'}
/> />
); );
}; };

View File

@ -5,6 +5,7 @@ import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButt
import { BrushSize } from 'features/regionalPrompts/components/BrushSize'; import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem'; import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem';
import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage'; import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage';
import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
const selectLayerIdsReversed = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => const selectLayerIdsReversed = createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
@ -18,6 +19,7 @@ export const RegionalPromptsEditor = () => {
<Flex flexDir="column" w={200} gap={4}> <Flex flexDir="column" w={200} gap={4}>
<AddLayerButton /> <AddLayerButton />
<BrushSize /> <BrushSize />
<ToolChooser />
{layerIdsReversed.map((id) => ( {layerIdsReversed.map((id) => (
<LayerListItem key={id} id={id} /> <LayerListItem key={id} id={id} />
))} ))}

View File

@ -0,0 +1,33 @@
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { toolChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react';
import { PiEraserBold, PiPaintBrushBold } from 'react-icons/pi';
export const ToolChooser: React.FC = () => {
const tool = useAppSelector((s) => s.regionalPrompts.tool);
const dispatch = useAppDispatch();
const setToolToBrush = useCallback(() => {
dispatch(toolChanged('brush'));
}, [dispatch]);
const setToolToEraser = useCallback(() => {
dispatch(toolChanged('eraser'));
}, [dispatch]);
return (
<ButtonGroup isAttached>
<IconButton
aria-label="Brush tool"
icon={<PiPaintBrushBold />}
variant={tool === 'brush' ? 'solid' : 'outline'}
onClick={setToolToBrush}
/>
<IconButton
aria-label="Eraser tool"
icon={<PiEraserBold />}
variant={tool === 'eraser' ? 'solid' : 'outline'}
onClick={setToolToEraser}
/>
</ButtonGroup>
);
};

View File

@ -1,10 +1,10 @@
import { getStore } from 'app/store/nanostores/store';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } 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,
lineAdded, lineAdded,
pointsAdded, pointsAdded,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
@ -13,6 +13,8 @@ import type { KonvaEventObject } from 'konva/lib/Node';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
const getTool = () => getStore().getState().regionalPrompts.tool;
const getIsFocused = (stage: Konva.Stage) => { const getIsFocused = (stage: Konva.Stage) => {
return stage.container().contains(document.activeElement); return stage.container().contains(document.activeElement);
}; };
@ -38,7 +40,8 @@ export const useMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) =>
return; return;
} }
$isMouseDown.set(true); $isMouseDown.set(true);
if ($tool.get() === 'brush') { const tool = getTool();
if (tool === 'brush' || tool === 'eraser') {
dispatch(lineAdded([pos.x, pos.y])); dispatch(lineAdded([pos.x, pos.y]));
} }
}, },
@ -54,7 +57,8 @@ export const useMouseUp = (stageRef: MutableRefObject<Konva.Stage | null>) => {
if (!stageRef.current) { if (!stageRef.current) {
return; return;
} }
if ($tool.get() === 'brush' && $isMouseDown.get()) { const tool = getTool();
if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) {
// Add another point to the last line. // Add another point to the last line.
$isMouseDown.set(false); $isMouseDown.set(false);
const pos = syncCursorPos(stageRef.current); const pos = syncCursorPos(stageRef.current);
@ -80,7 +84,13 @@ export const useMouseMove = (stageRef: MutableRefObject<Konva.Stage | null>) =>
if (!pos) { if (!pos) {
return; return;
} }
if (getIsFocused(stageRef.current) && $isMouseOver.get() && $isMouseDown.get() && $tool.get() === 'brush') { const tool = getTool();
if (
getIsFocused(stageRef.current) &&
$isMouseOver.get() &&
$isMouseDown.get() &&
(tool === 'brush' || tool === 'eraser')
) {
dispatch(pointsAdded([pos.x, pos.y])); dispatch(pointsAdded([pos.x, pos.y]));
} }
}, },
@ -123,7 +133,8 @@ export const useMouseEnter = (stageRef: MutableRefObject<Konva.Stage | null>) =>
$isMouseDown.set(false); $isMouseDown.set(false);
} else { } else {
$isMouseDown.set(true); $isMouseDown.set(true);
if ($tool.get() === 'brush') { const tool = getTool();
if (tool === 'brush' || tool === 'eraser') {
dispatch(lineAdded([pos.x, pos.y])); dispatch(lineAdded([pos.x, pos.y]));
} }
} }

View File

@ -9,6 +9,8 @@ import type { RgbColor } from 'react-colorful';
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';
type LayerObjectBase = { type LayerObjectBase = {
id: string; id: string;
isSelected: boolean; isSelected: boolean;
@ -25,6 +27,7 @@ type ImageObject = LayerObjectBase & {
export type LineObject = LayerObjectBase & { export type LineObject = LayerObjectBase & {
kind: 'line'; kind: 'line';
tool: Tool;
strokeWidth: number; strokeWidth: number;
points: number[]; points: number[];
}; };
@ -53,10 +56,9 @@ type PromptRegionLayer = LayerBase & {
type Layer = PromptRegionLayer; type Layer = PromptRegionLayer;
type Tool = 'brush';
type RegionalPromptsState = { type RegionalPromptsState = {
_version: 1; _version: 1;
tool: Tool;
selectedLayer: string | null; selectedLayer: string | null;
layers: PromptRegionLayer[]; layers: PromptRegionLayer[];
brushSize: number; brushSize: number;
@ -64,6 +66,7 @@ type RegionalPromptsState = {
const initialRegionalPromptsState: RegionalPromptsState = { const initialRegionalPromptsState: RegionalPromptsState = {
_version: 1, _version: 1,
tool: 'brush',
selectedLayer: null, selectedLayer: null,
brushSize: 40, brushSize: 40,
layers: [], layers: [],
@ -144,7 +147,7 @@ export const regionalPromptsSlice = createSlice({
if (!selectedLayer || selectedLayer.kind !== 'promptRegionLayer') { if (!selectedLayer || selectedLayer.kind !== 'promptRegionLayer') {
return; return;
} }
selectedLayer.objects.push(buildLine(action.meta.id, action.payload, state.brushSize)); selectedLayer.objects.push(buildLine(action.meta.id, action.payload, state.brushSize, state.tool));
}, },
prepare: (payload: number[]) => ({ payload, meta: { id: uuidv4() } }), prepare: (payload: number[]) => ({ payload, meta: { id: uuidv4() } }),
}, },
@ -162,6 +165,9 @@ 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;
},
}, },
}); });
@ -190,9 +196,10 @@ const buildLayer = (id: string, kind: Layer['kind'], layerCount: number): Layer
assert(false, `Unknown layer kind: ${kind}`); assert(false, `Unknown layer kind: ${kind}`);
}; };
const buildLine = (id: string, points: number[], brushSize: number): LineObject => ({ const buildLine = (id: string, points: number[], brushSize: number, tool: Tool): LineObject => ({
isSelected: false, isSelected: false,
kind: 'line', kind: 'line',
tool,
id, id,
points, points,
strokeWidth: brushSize, strokeWidth: brushSize,
@ -213,6 +220,7 @@ export const {
layerMovedToFront, layerMovedToFront,
layerMovedBackward, layerMovedBackward,
layerMovedToBack, layerMovedToBack,
toolChanged,
} = regionalPromptsSlice.actions; } = regionalPromptsSlice.actions;
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts; export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
@ -232,5 +240,4 @@ export const regionalPromptsPersistConfig: PersistConfig<RegionalPromptsState> =
export const $isMouseDown = atom(false); export const $isMouseDown = atom(false);
export const $isMouseOver = atom(false); export const $isMouseOver = atom(false);
export const $cursorPosition = atom<Vector2d | null>(null); export const $cursorPosition = atom<Vector2d | null>(null);
export const $tool = atom<Tool>('brush');
export const $stage = atom<Konva.Stage | null>(null); export const $stage = atom<Konva.Stage | null>(null);