mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): re-implement with imperative konva api (wip)
This commit is contained in:
parent
bbbb5479e8
commit
525e6d697c
@ -6,6 +6,11 @@ import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
|
||||
import {
|
||||
$cursorPosition,
|
||||
BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
BRUSH_PREVIEW_FILL_ID,
|
||||
BRUSH_PREVIEW_LAYER_ID,
|
||||
getPromptRegionLayerObjectGroupId,
|
||||
layerBboxChanged,
|
||||
layerSelected,
|
||||
layerTranslated,
|
||||
@ -17,6 +22,7 @@ import Konva from 'konva';
|
||||
import type { Node, NodeConfig } from 'konva/lib/Node';
|
||||
import { atom } from 'nanostores';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { useMouseDown, useMouseEnter, useMouseLeave, useMouseMove, useMouseUp } from './mouseEventHooks';
|
||||
|
||||
@ -29,10 +35,6 @@ type Props = {
|
||||
export const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
||||
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
|
||||
|
||||
const BRUSH_PREVIEW_FILL = 'brushPreviewFill';
|
||||
const BRUSH_PREVIEW_OUTLINE_INNER = 'brushPreviewOutlineInner';
|
||||
const BRUSH_PREVIEW_OUTLINE_OUTER = 'brushPreviewOutlineOuter';
|
||||
|
||||
const isKonvaLayer = (node: Node<NodeConfig>): node is Konva.Layer => node.nodeType === 'Layer';
|
||||
const isKonvaLine = (node: Node<NodeConfig>): node is Konva.Line => node.nodeType === 'Line';
|
||||
const isKonvaGroup = (node: Node<NodeConfig>): node is Konva.Group => node.nodeType === 'Group';
|
||||
@ -41,8 +43,8 @@ const isKonvaRect = (node: Node<NodeConfig>): node is Konva.Rect => node.nodeTyp
|
||||
const $brushPreviewNodes = atom<{
|
||||
layer: Konva.Layer;
|
||||
fill: Konva.Circle;
|
||||
outlineInner: Konva.Circle;
|
||||
outlineOuter: Konva.Circle;
|
||||
borderInner: Konva.Circle;
|
||||
borderOuter: Konva.Circle;
|
||||
} | null>(null);
|
||||
|
||||
export const LogicalStage = (props: Props) => {
|
||||
@ -68,32 +70,32 @@ export const LogicalStage = (props: Props) => {
|
||||
container: props.container,
|
||||
});
|
||||
|
||||
const brushPreviewLayer = new Konva.Layer({ id: 'brushPreviewLayer' });
|
||||
const brushPreviewLayer = new Konva.Layer({ id: BRUSH_PREVIEW_LAYER_ID });
|
||||
stage.add(brushPreviewLayer);
|
||||
const fill = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_FILL,
|
||||
id: BRUSH_PREVIEW_FILL_ID,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
strokeHitEnabled: false,
|
||||
});
|
||||
const outlineInner = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_OUTLINE_INNER,
|
||||
const borderInner = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
listening: false,
|
||||
stroke: 'rgba(0,0,0,1)',
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
const outlineOuter = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_OUTLINE_OUTER,
|
||||
const borderOuter = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
listening: false,
|
||||
stroke: 'rgba(255,255,255,0.8)',
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
brushPreviewLayer.add(fill);
|
||||
brushPreviewLayer.add(outlineInner);
|
||||
brushPreviewLayer.add(outlineOuter);
|
||||
$brushPreviewNodes.set({ layer: brushPreviewLayer, fill, outlineInner, outlineOuter });
|
||||
brushPreviewLayer.add(borderInner);
|
||||
brushPreviewLayer.add(borderOuter);
|
||||
$brushPreviewNodes.set({ layer: brushPreviewLayer, fill, borderInner: borderInner, borderOuter: borderOuter });
|
||||
|
||||
$stage.set(stage);
|
||||
|
||||
@ -156,8 +158,8 @@ export const LogicalStage = (props: Props) => {
|
||||
fill,
|
||||
globalCompositeOperation: state.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
});
|
||||
brushPreviewNodes.outlineInner.setAttrs({ x: cursorPosition.x, y: cursorPosition.y, radius: state.brushSize / 2 });
|
||||
brushPreviewNodes.outlineOuter.setAttrs({
|
||||
brushPreviewNodes.borderInner.setAttrs({ x: cursorPosition.x, y: cursorPosition.y, radius: state.brushSize / 2 });
|
||||
brushPreviewNodes.borderOuter.setAttrs({
|
||||
x: cursorPosition.x,
|
||||
y: cursorPosition.y,
|
||||
radius: state.brushSize / 2 + 1,
|
||||
@ -189,6 +191,8 @@ export const LogicalStage = (props: Props) => {
|
||||
name: REGIONAL_PROMPT_LAYER_NAME,
|
||||
draggable: true,
|
||||
listening: reduxLayer.id === state.selectedLayer,
|
||||
x: reduxLayer.x,
|
||||
y: reduxLayer.y,
|
||||
});
|
||||
konvaLayer.on('dragmove', function (e) {
|
||||
dispatch(
|
||||
@ -212,12 +216,22 @@ export const LogicalStage = (props: Props) => {
|
||||
return pos;
|
||||
});
|
||||
stage.add(konvaLayer);
|
||||
konvaLayer.add(
|
||||
new Konva.Group({
|
||||
id: getPromptRegionLayerObjectGroupId(reduxLayer.id, uuidv4()),
|
||||
name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
|
||||
listening: false,
|
||||
})
|
||||
);
|
||||
$brushPreviewNodes.get()?.layer.moveToTop();
|
||||
} else {
|
||||
konvaLayer.listening(reduxLayer.id === state.selectedLayer);
|
||||
konvaLayer.x(reduxLayer.x);
|
||||
konvaLayer.y(reduxLayer.y);
|
||||
}
|
||||
|
||||
const color = rgbColorToString(reduxLayer.color);
|
||||
const konvaObjectGroup = konvaLayer.findOne(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`) as Konva.Group;
|
||||
|
||||
// Remove deleted objects
|
||||
const objectIds = reduxLayer.objects.map((o) => o.id);
|
||||
@ -228,35 +242,41 @@ export const LogicalStage = (props: Props) => {
|
||||
}
|
||||
|
||||
for (const reduxObject of reduxLayer.objects) {
|
||||
// TODO: Handle rects, images, etc
|
||||
if (reduxObject.kind !== 'line') {
|
||||
return;
|
||||
}
|
||||
let konvaObject = stage.findOne(`#${reduxObject.id}`) as Konva.Line | undefined;
|
||||
const konvaObject = stage.findOne(`#${reduxObject.id}`) as Konva.Line | undefined;
|
||||
|
||||
if (!konvaObject) {
|
||||
konvaObject = new Konva.Line({
|
||||
id: reduxObject.id,
|
||||
key: reduxObject.id,
|
||||
name: `${reduxLayer.id}-object`,
|
||||
points: reduxObject.points,
|
||||
strokeWidth: reduxObject.strokeWidth,
|
||||
stroke: color,
|
||||
tension: 0,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
listening: false,
|
||||
visible: reduxLayer.isVisible,
|
||||
});
|
||||
konvaLayer.add(konvaObject);
|
||||
// This object hasn't been added to the konva state yet.
|
||||
konvaObjectGroup.add(
|
||||
new Konva.Line({
|
||||
id: reduxObject.id,
|
||||
key: reduxObject.id,
|
||||
name: `${reduxLayer.id}-object`,
|
||||
points: reduxObject.points,
|
||||
strokeWidth: reduxObject.strokeWidth,
|
||||
stroke: color,
|
||||
tension: 0,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
listening: false,
|
||||
visible: reduxLayer.isVisible,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Only update the points if they have changed. The point values are never mutated, they are only added to the array.
|
||||
if (konvaObject.points().length !== reduxObject.points.length) {
|
||||
konvaObject.points(reduxObject.points);
|
||||
}
|
||||
// Only update the color if it has changed.
|
||||
if (konvaObject.stroke() !== color) {
|
||||
konvaObject.stroke(color);
|
||||
}
|
||||
// Only update layer visibility if it has changed.
|
||||
if (konvaObject.visible() !== reduxLayer.isVisible) {
|
||||
konvaObject.visible(reduxLayer.isVisible);
|
||||
}
|
||||
@ -269,6 +289,14 @@ export const LogicalStage = (props: Props) => {
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
stage.container().style.cursor = state.tool === 'move' ? 'default' : 'none';
|
||||
}, [stage, state.tool]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
console.log('bbox effect');
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.tool !== 'move') {
|
||||
// Tool was just changed to something other than move - hide all layer bounding boxes
|
||||
|
@ -83,12 +83,22 @@ export const regionalPromptsSlice = createSlice({
|
||||
initialState: initialRegionalPromptsState,
|
||||
reducers: {
|
||||
layerAdded: {
|
||||
reducer: (state, action: PayloadAction<Layer['kind'], string, { id: string }>) => {
|
||||
const newLayer = buildLayer(action.meta.id, action.payload, state.layers.length);
|
||||
state.layers.push(newLayer);
|
||||
state.selectedLayer = newLayer.id;
|
||||
reducer: (state, action: PayloadAction<Layer['kind'], string, { uuid: string; color: RgbColor }>) => {
|
||||
const layer: PromptRegionLayer = {
|
||||
id: getPromptRegionLayerId(action.meta.uuid),
|
||||
isVisible: true,
|
||||
bbox: null,
|
||||
kind: action.payload,
|
||||
prompt: '',
|
||||
objects: [],
|
||||
color: action.meta.color,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
state.layers.push(layer);
|
||||
state.selectedLayer = layer.id;
|
||||
},
|
||||
prepare: (payload: Layer['kind']) => ({ payload, meta: { id: uuidv4() } }),
|
||||
prepare: (payload: Layer['kind']) => ({ payload, meta: { uuid: uuidv4(), color: LayerColors.next() } }),
|
||||
},
|
||||
layerSelected: (state, action: PayloadAction<string>) => {
|
||||
state.selectedLayer = action.payload;
|
||||
@ -166,20 +176,21 @@ export const regionalPromptsSlice = createSlice({
|
||||
layer.color = color;
|
||||
},
|
||||
lineAdded: {
|
||||
reducer: (state, action: PayloadAction<[number, number], string, { id: string }>) => {
|
||||
reducer: (state, action: PayloadAction<[number, number], string, { uuid: string }>) => {
|
||||
const layer = state.layers.find((l) => l.id === state.selectedLayer);
|
||||
if (!layer || layer.kind !== 'promptRegionLayer') {
|
||||
return;
|
||||
}
|
||||
const lineId = getPromptRegionLayerLineId(layer.id, action.meta.uuid);
|
||||
layer.objects.push({
|
||||
kind: 'line',
|
||||
tool: state.tool,
|
||||
id: action.meta.id,
|
||||
id: lineId,
|
||||
points: [action.payload[0] - layer.x, action.payload[1] - layer.y],
|
||||
strokeWidth: state.brushSize,
|
||||
});
|
||||
},
|
||||
prepare: (payload: [number, number]) => ({ payload, meta: { id: uuidv4() } }),
|
||||
prepare: (payload: [number, number]) => ({ payload, meta: { uuid: uuidv4() } }),
|
||||
},
|
||||
pointsAdded: (state, action: PayloadAction<[number, number]>) => {
|
||||
const layer = state.layers.find((l) => l.id === state.selectedLayer);
|
||||
@ -204,33 +215,29 @@ export const regionalPromptsSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
{ r: 200, g: 0, b: 0 },
|
||||
{ r: 0, g: 200, b: 0 },
|
||||
{ r: 0, g: 0, b: 200 },
|
||||
{ r: 200, g: 200, b: 0 },
|
||||
{ r: 0, g: 200, b: 200 },
|
||||
{ r: 200, g: 0, b: 200 },
|
||||
];
|
||||
|
||||
const buildLayer = (id: string, kind: Layer['kind'], layerCount: number): Layer => {
|
||||
if (kind === 'promptRegionLayer') {
|
||||
const color = DEFAULT_COLORS[layerCount % DEFAULT_COLORS.length];
|
||||
assert(color, 'Color not found');
|
||||
return {
|
||||
id,
|
||||
isVisible: true,
|
||||
bbox: null,
|
||||
kind,
|
||||
prompt: '',
|
||||
objects: [],
|
||||
color,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
/**
|
||||
* This class is used to cycle through a set of colors for the prompt region layers.
|
||||
*/
|
||||
class LayerColors {
|
||||
static COLORS: RgbColor[] = [
|
||||
{ r: 200, g: 0, b: 0 },
|
||||
{ r: 0, g: 200, b: 0 },
|
||||
{ r: 0, g: 0, b: 200 },
|
||||
{ r: 200, g: 200, b: 0 },
|
||||
{ r: 0, g: 200, b: 200 },
|
||||
{ r: 200, g: 0, b: 200 },
|
||||
];
|
||||
static i = this.COLORS.length - 1;
|
||||
/**
|
||||
* Get the next color in the sequence.
|
||||
*/
|
||||
static next(): RgbColor {
|
||||
this.i = (this.i + 1) % this.COLORS.length;
|
||||
const color = this.COLORS[this.i];
|
||||
assert(color);
|
||||
return color;
|
||||
}
|
||||
assert(false, `Unknown layer kind: ${kind}`);
|
||||
};
|
||||
}
|
||||
|
||||
export const {
|
||||
layerAdded,
|
||||
@ -276,5 +283,12 @@ export const getStage = (): Stage => {
|
||||
assert(stage);
|
||||
return stage;
|
||||
};
|
||||
export const BRUSH_PREVIEW_LAYER_ID = 'brushPreviewLayer';
|
||||
export const BRUSH_PREVIEW_FILL_ID = 'brushPreviewFill';
|
||||
export const BRUSH_PREVIEW_BORDER_INNER_ID = 'brushPreviewBorderInner';
|
||||
export const BRUSH_PREVIEW_BORDER_OUTER_ID = 'brushPreviewBorderOuter';
|
||||
export const REGIONAL_PROMPT_LAYER_NAME = 'regionalPromptLayer';
|
||||
export const REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME = 'regionalPromptLayerObjectGroup';
|
||||
export const REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME = 'regionalPromptLayerObjectGroup';
|
||||
export const getPromptRegionLayerId = (layerId: string) => `layer_${layerId}`;
|
||||
export const getPromptRegionLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
||||
export const getPromptRegionLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
|
||||
|
Loading…
Reference in New Issue
Block a user