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

@@ -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);