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

View File

@ -24,6 +24,7 @@ export const LineComponent = ({ line, color }: Props) => {
lineJoin="round"
shadowForStrokeEnabled={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 { LayerListItem } from 'features/regionalPrompts/components/LayerListItem';
import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage';
import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
const selectLayerIdsReversed = createSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
@ -18,6 +19,7 @@ export const RegionalPromptsEditor = () => {
<Flex flexDir="column" w={200} gap={4}>
<AddLayerButton />
<BrushSize />
<ToolChooser />
{layerIdsReversed.map((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 getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import {
$cursorPosition,
$isMouseDown,
$isMouseOver,
$tool,
lineAdded,
pointsAdded,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
@ -13,6 +13,8 @@ import type { KonvaEventObject } from 'konva/lib/Node';
import type { MutableRefObject } from 'react';
import { useCallback } from 'react';
const getTool = () => getStore().getState().regionalPrompts.tool;
const getIsFocused = (stage: Konva.Stage) => {
return stage.container().contains(document.activeElement);
};
@ -38,7 +40,8 @@ export const useMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) =>
return;
}
$isMouseDown.set(true);
if ($tool.get() === 'brush') {
const tool = getTool();
if (tool === 'brush' || tool === 'eraser') {
dispatch(lineAdded([pos.x, pos.y]));
}
},
@ -54,7 +57,8 @@ export const useMouseUp = (stageRef: MutableRefObject<Konva.Stage | null>) => {
if (!stageRef.current) {
return;
}
if ($tool.get() === 'brush' && $isMouseDown.get()) {
const tool = getTool();
if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) {
// Add another point to the last line.
$isMouseDown.set(false);
const pos = syncCursorPos(stageRef.current);
@ -80,7 +84,13 @@ export const useMouseMove = (stageRef: MutableRefObject<Konva.Stage | null>) =>
if (!pos) {
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]));
}
},
@ -123,7 +133,8 @@ export const useMouseEnter = (stageRef: MutableRefObject<Konva.Stage | null>) =>
$isMouseDown.set(false);
} else {
$isMouseDown.set(true);
if ($tool.get() === 'brush') {
const tool = getTool();
if (tool === 'brush' || tool === 'eraser') {
dispatch(lineAdded([pos.x, pos.y]));
}
}

View File

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