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 getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
|
||||||
import {
|
import {
|
||||||
$cursorPosition,
|
$cursorPosition,
|
||||||
|
BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||||
|
BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||||
|
BRUSH_PREVIEW_FILL_ID,
|
||||||
|
BRUSH_PREVIEW_LAYER_ID,
|
||||||
|
getPromptRegionLayerObjectGroupId,
|
||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
layerSelected,
|
layerSelected,
|
||||||
layerTranslated,
|
layerTranslated,
|
||||||
@ -17,6 +22,7 @@ import Konva from 'konva';
|
|||||||
import type { Node, NodeConfig } from 'konva/lib/Node';
|
import type { Node, NodeConfig } from 'konva/lib/Node';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { useMouseDown, useMouseEnter, useMouseLeave, useMouseMove, useMouseUp } from './mouseEventHooks';
|
import { useMouseDown, useMouseEnter, useMouseLeave, useMouseMove, useMouseUp } from './mouseEventHooks';
|
||||||
|
|
||||||
@ -29,10 +35,6 @@ type Props = {
|
|||||||
export const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
export const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
||||||
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
|
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 isKonvaLayer = (node: Node<NodeConfig>): node is Konva.Layer => node.nodeType === 'Layer';
|
||||||
const isKonvaLine = (node: Node<NodeConfig>): node is Konva.Line => node.nodeType === 'Line';
|
const isKonvaLine = (node: Node<NodeConfig>): node is Konva.Line => node.nodeType === 'Line';
|
||||||
const isKonvaGroup = (node: Node<NodeConfig>): node is Konva.Group => node.nodeType === 'Group';
|
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<{
|
const $brushPreviewNodes = atom<{
|
||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
fill: Konva.Circle;
|
fill: Konva.Circle;
|
||||||
outlineInner: Konva.Circle;
|
borderInner: Konva.Circle;
|
||||||
outlineOuter: Konva.Circle;
|
borderOuter: Konva.Circle;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
export const LogicalStage = (props: Props) => {
|
export const LogicalStage = (props: Props) => {
|
||||||
@ -68,32 +70,32 @@ export const LogicalStage = (props: Props) => {
|
|||||||
container: props.container,
|
container: props.container,
|
||||||
});
|
});
|
||||||
|
|
||||||
const brushPreviewLayer = new Konva.Layer({ id: 'brushPreviewLayer' });
|
const brushPreviewLayer = new Konva.Layer({ id: BRUSH_PREVIEW_LAYER_ID });
|
||||||
stage.add(brushPreviewLayer);
|
stage.add(brushPreviewLayer);
|
||||||
const fill = new Konva.Circle({
|
const fill = new Konva.Circle({
|
||||||
id: BRUSH_PREVIEW_FILL,
|
id: BRUSH_PREVIEW_FILL_ID,
|
||||||
listening: false,
|
listening: false,
|
||||||
strokeEnabled: false,
|
strokeEnabled: false,
|
||||||
strokeHitEnabled: false,
|
strokeHitEnabled: false,
|
||||||
});
|
});
|
||||||
const outlineInner = new Konva.Circle({
|
const borderInner = new Konva.Circle({
|
||||||
id: BRUSH_PREVIEW_OUTLINE_INNER,
|
id: BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||||
listening: false,
|
listening: false,
|
||||||
stroke: 'rgba(0,0,0,1)',
|
stroke: 'rgba(0,0,0,1)',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
});
|
});
|
||||||
const outlineOuter = new Konva.Circle({
|
const borderOuter = new Konva.Circle({
|
||||||
id: BRUSH_PREVIEW_OUTLINE_OUTER,
|
id: BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||||
listening: false,
|
listening: false,
|
||||||
stroke: 'rgba(255,255,255,0.8)',
|
stroke: 'rgba(255,255,255,0.8)',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
});
|
});
|
||||||
brushPreviewLayer.add(fill);
|
brushPreviewLayer.add(fill);
|
||||||
brushPreviewLayer.add(outlineInner);
|
brushPreviewLayer.add(borderInner);
|
||||||
brushPreviewLayer.add(outlineOuter);
|
brushPreviewLayer.add(borderOuter);
|
||||||
$brushPreviewNodes.set({ layer: brushPreviewLayer, fill, outlineInner, outlineOuter });
|
$brushPreviewNodes.set({ layer: brushPreviewLayer, fill, borderInner: borderInner, borderOuter: borderOuter });
|
||||||
|
|
||||||
$stage.set(stage);
|
$stage.set(stage);
|
||||||
|
|
||||||
@ -156,8 +158,8 @@ export const LogicalStage = (props: Props) => {
|
|||||||
fill,
|
fill,
|
||||||
globalCompositeOperation: state.tool === 'brush' ? 'source-over' : 'destination-out',
|
globalCompositeOperation: state.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
});
|
});
|
||||||
brushPreviewNodes.outlineInner.setAttrs({ x: cursorPosition.x, y: cursorPosition.y, radius: state.brushSize / 2 });
|
brushPreviewNodes.borderInner.setAttrs({ x: cursorPosition.x, y: cursorPosition.y, radius: state.brushSize / 2 });
|
||||||
brushPreviewNodes.outlineOuter.setAttrs({
|
brushPreviewNodes.borderOuter.setAttrs({
|
||||||
x: cursorPosition.x,
|
x: cursorPosition.x,
|
||||||
y: cursorPosition.y,
|
y: cursorPosition.y,
|
||||||
radius: state.brushSize / 2 + 1,
|
radius: state.brushSize / 2 + 1,
|
||||||
@ -189,6 +191,8 @@ export const LogicalStage = (props: Props) => {
|
|||||||
name: REGIONAL_PROMPT_LAYER_NAME,
|
name: REGIONAL_PROMPT_LAYER_NAME,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
listening: reduxLayer.id === state.selectedLayer,
|
listening: reduxLayer.id === state.selectedLayer,
|
||||||
|
x: reduxLayer.x,
|
||||||
|
y: reduxLayer.y,
|
||||||
});
|
});
|
||||||
konvaLayer.on('dragmove', function (e) {
|
konvaLayer.on('dragmove', function (e) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -212,12 +216,22 @@ export const LogicalStage = (props: Props) => {
|
|||||||
return pos;
|
return pos;
|
||||||
});
|
});
|
||||||
stage.add(konvaLayer);
|
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();
|
$brushPreviewNodes.get()?.layer.moveToTop();
|
||||||
} else {
|
} else {
|
||||||
konvaLayer.listening(reduxLayer.id === state.selectedLayer);
|
konvaLayer.listening(reduxLayer.id === state.selectedLayer);
|
||||||
|
konvaLayer.x(reduxLayer.x);
|
||||||
|
konvaLayer.y(reduxLayer.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = rgbColorToString(reduxLayer.color);
|
const color = rgbColorToString(reduxLayer.color);
|
||||||
|
const konvaObjectGroup = konvaLayer.findOne(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`) as Konva.Group;
|
||||||
|
|
||||||
// Remove deleted objects
|
// Remove deleted objects
|
||||||
const objectIds = reduxLayer.objects.map((o) => o.id);
|
const objectIds = reduxLayer.objects.map((o) => o.id);
|
||||||
@ -228,13 +242,16 @@ export const LogicalStage = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const reduxObject of reduxLayer.objects) {
|
for (const reduxObject of reduxLayer.objects) {
|
||||||
|
// TODO: Handle rects, images, etc
|
||||||
if (reduxObject.kind !== 'line') {
|
if (reduxObject.kind !== 'line') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let konvaObject = stage.findOne(`#${reduxObject.id}`) as Konva.Line | undefined;
|
const konvaObject = stage.findOne(`#${reduxObject.id}`) as Konva.Line | undefined;
|
||||||
|
|
||||||
if (!konvaObject) {
|
if (!konvaObject) {
|
||||||
konvaObject = new Konva.Line({
|
// This object hasn't been added to the konva state yet.
|
||||||
|
konvaObjectGroup.add(
|
||||||
|
new Konva.Line({
|
||||||
id: reduxObject.id,
|
id: reduxObject.id,
|
||||||
key: reduxObject.id,
|
key: reduxObject.id,
|
||||||
name: `${reduxLayer.id}-object`,
|
name: `${reduxLayer.id}-object`,
|
||||||
@ -248,15 +265,18 @@ export const LogicalStage = (props: Props) => {
|
|||||||
globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
|
globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
listening: false,
|
listening: false,
|
||||||
visible: reduxLayer.isVisible,
|
visible: reduxLayer.isVisible,
|
||||||
});
|
})
|
||||||
konvaLayer.add(konvaObject);
|
);
|
||||||
} else {
|
} 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) {
|
if (konvaObject.points().length !== reduxObject.points.length) {
|
||||||
konvaObject.points(reduxObject.points);
|
konvaObject.points(reduxObject.points);
|
||||||
}
|
}
|
||||||
|
// Only update the color if it has changed.
|
||||||
if (konvaObject.stroke() !== color) {
|
if (konvaObject.stroke() !== color) {
|
||||||
konvaObject.stroke(color);
|
konvaObject.stroke(color);
|
||||||
}
|
}
|
||||||
|
// Only update layer visibility if it has changed.
|
||||||
if (konvaObject.visible() !== reduxLayer.isVisible) {
|
if (konvaObject.visible() !== reduxLayer.isVisible) {
|
||||||
konvaObject.visible(reduxLayer.isVisible);
|
konvaObject.visible(reduxLayer.isVisible);
|
||||||
}
|
}
|
||||||
@ -269,6 +289,14 @@ export const LogicalStage = (props: Props) => {
|
|||||||
if (!stage) {
|
if (!stage) {
|
||||||
return;
|
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') {
|
if (state.tool !== 'move') {
|
||||||
// Tool was just changed to something other than move - hide all layer bounding boxes
|
// Tool was just changed to something other than move - hide all layer bounding boxes
|
||||||
|
@ -83,12 +83,22 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
initialState: initialRegionalPromptsState,
|
initialState: initialRegionalPromptsState,
|
||||||
reducers: {
|
reducers: {
|
||||||
layerAdded: {
|
layerAdded: {
|
||||||
reducer: (state, action: PayloadAction<Layer['kind'], string, { id: string }>) => {
|
reducer: (state, action: PayloadAction<Layer['kind'], string, { uuid: string; color: RgbColor }>) => {
|
||||||
const newLayer = buildLayer(action.meta.id, action.payload, state.layers.length);
|
const layer: PromptRegionLayer = {
|
||||||
state.layers.push(newLayer);
|
id: getPromptRegionLayerId(action.meta.uuid),
|
||||||
state.selectedLayer = newLayer.id;
|
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>) => {
|
layerSelected: (state, action: PayloadAction<string>) => {
|
||||||
state.selectedLayer = action.payload;
|
state.selectedLayer = action.payload;
|
||||||
@ -166,20 +176,21 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
layer.color = color;
|
layer.color = color;
|
||||||
},
|
},
|
||||||
lineAdded: {
|
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);
|
const layer = state.layers.find((l) => l.id === state.selectedLayer);
|
||||||
if (!layer || layer.kind !== 'promptRegionLayer') {
|
if (!layer || layer.kind !== 'promptRegionLayer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const lineId = getPromptRegionLayerLineId(layer.id, action.meta.uuid);
|
||||||
layer.objects.push({
|
layer.objects.push({
|
||||||
kind: 'line',
|
kind: 'line',
|
||||||
tool: state.tool,
|
tool: state.tool,
|
||||||
id: action.meta.id,
|
id: lineId,
|
||||||
points: [action.payload[0] - layer.x, action.payload[1] - layer.y],
|
points: [action.payload[0] - layer.x, action.payload[1] - layer.y],
|
||||||
strokeWidth: state.brushSize,
|
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]>) => {
|
pointsAdded: (state, action: PayloadAction<[number, number]>) => {
|
||||||
const layer = state.layers.find((l) => l.id === state.selectedLayer);
|
const layer = state.layers.find((l) => l.id === state.selectedLayer);
|
||||||
@ -204,33 +215,29 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_COLORS = [
|
/**
|
||||||
|
* 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: 200, g: 0, b: 0 },
|
||||||
{ r: 0, g: 200, b: 0 },
|
{ r: 0, g: 200, b: 0 },
|
||||||
{ r: 0, g: 0, b: 200 },
|
{ r: 0, g: 0, b: 200 },
|
||||||
{ r: 200, g: 200, b: 0 },
|
{ r: 200, g: 200, b: 0 },
|
||||||
{ r: 0, g: 200, b: 200 },
|
{ r: 0, g: 200, b: 200 },
|
||||||
{ r: 200, g: 0, b: 200 },
|
{ r: 200, g: 0, b: 200 },
|
||||||
];
|
];
|
||||||
|
static i = this.COLORS.length - 1;
|
||||||
const buildLayer = (id: string, kind: Layer['kind'], layerCount: number): Layer => {
|
/**
|
||||||
if (kind === 'promptRegionLayer') {
|
* Get the next color in the sequence.
|
||||||
const color = DEFAULT_COLORS[layerCount % DEFAULT_COLORS.length];
|
*/
|
||||||
assert(color, 'Color not found');
|
static next(): RgbColor {
|
||||||
return {
|
this.i = (this.i + 1) % this.COLORS.length;
|
||||||
id,
|
const color = this.COLORS[this.i];
|
||||||
isVisible: true,
|
assert(color);
|
||||||
bbox: null,
|
return color;
|
||||||
kind,
|
|
||||||
prompt: '',
|
|
||||||
objects: [],
|
|
||||||
color,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
assert(false, `Unknown layer kind: ${kind}`);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
layerAdded,
|
layerAdded,
|
||||||
@ -276,5 +283,12 @@ export const getStage = (): Stage => {
|
|||||||
assert(stage);
|
assert(stage);
|
||||||
return 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_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