mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip regional prompting UI
- Add eraser tool, applies per layer
This commit is contained in:
parent
822dfa77fc
commit
52ba4966c9
@ -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 = {
|
||||||
|
@ -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'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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} />
|
||||||
))}
|
))}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user