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
05deeb68fa
commit
ae7797f662
@ -4,6 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
|
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
|
||||||
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
|
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
|
||||||
|
import { StageComponent } from 'features/regionalPrompts/components/imperative/konvaApiDraft';
|
||||||
import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem';
|
import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem';
|
||||||
import { PromptLayerOpacity } from 'features/regionalPrompts/components/PromptLayerOpacity';
|
import { PromptLayerOpacity } from 'features/regionalPrompts/components/PromptLayerOpacity';
|
||||||
import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage';
|
import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage';
|
||||||
@ -37,7 +38,8 @@ export const RegionalPromptsEditor = memo(() => {
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex>
|
<Flex>
|
||||||
<RegionalPromptsStage />
|
<StageComponent />
|
||||||
|
{/* <RegionalPromptsStage /> */}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,235 @@
|
|||||||
|
import { chakra } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
|
||||||
|
import {
|
||||||
|
$cursorPosition,
|
||||||
|
layerBboxChanged,
|
||||||
|
layerSelected,
|
||||||
|
layerTranslated,
|
||||||
|
REGIONAL_PROMPT_LAYER_NAME,
|
||||||
|
REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import type { Node, NodeConfig } from 'konva/lib/Node';
|
||||||
|
import type { StageConfig } from 'konva/lib/Stage';
|
||||||
|
import { atom } from 'nanostores';
|
||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import { useMouseDown, useMouseEnter, useMouseLeave, useMouseMove, useMouseUp } from './mouseEventHooks';
|
||||||
|
|
||||||
|
export const $stage = atom<Konva.Stage | null>(null);
|
||||||
|
|
||||||
|
const initStage = (container: StageConfig['container']) => {
|
||||||
|
const stage = new Konva.Stage({
|
||||||
|
container,
|
||||||
|
});
|
||||||
|
$stage.set(stage);
|
||||||
|
|
||||||
|
const layer = new Konva.Layer();
|
||||||
|
const circle = new Konva.Circle({ id: 'cursor', radius: 5, fill: 'red' });
|
||||||
|
layer.add(circle);
|
||||||
|
stage.add(layer);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
||||||
|
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
|
||||||
|
|
||||||
|
export const LogicalStage = (props: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const width = useAppSelector((s) => s.generation.width);
|
||||||
|
const height = useAppSelector((s) => s.generation.height);
|
||||||
|
const state = useAppSelector((s) => s.regionalPrompts);
|
||||||
|
const stage = useStore($stage);
|
||||||
|
const onMouseDown = useMouseDown();
|
||||||
|
const onMouseUp = useMouseUp();
|
||||||
|
const onMouseMove = useMouseMove();
|
||||||
|
const onMouseEnter = useMouseEnter();
|
||||||
|
const onMouseLeave = useMouseLeave();
|
||||||
|
const cursorPosition = useStore($cursorPosition);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
console.log('init effect');
|
||||||
|
if (!props.container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initStage(props.container);
|
||||||
|
return () => {
|
||||||
|
const stage = $stage.get();
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stage.destroy();
|
||||||
|
};
|
||||||
|
}, [props.container]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
console.log('event effect');
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stage.on('mousedown', onMouseDown);
|
||||||
|
stage.on('mouseup', onMouseUp);
|
||||||
|
stage.on('mousemove', onMouseMove);
|
||||||
|
stage.on('mouseenter', onMouseEnter);
|
||||||
|
stage.on('mouseleave', onMouseLeave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stage.off('mousedown', onMouseDown);
|
||||||
|
stage.off('mouseup', onMouseUp);
|
||||||
|
stage.off('mousemove', onMouseMove);
|
||||||
|
stage.off('mouseenter', onMouseEnter);
|
||||||
|
stage.off('mouseleave', onMouseLeave);
|
||||||
|
};
|
||||||
|
}, [stage, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
console.log('stage dims effect');
|
||||||
|
if (!stage || !props.container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stage.width(width);
|
||||||
|
stage.height(height);
|
||||||
|
}, [stage, width, height, props.container]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
console.log('cursor effect');
|
||||||
|
if (!stage || !cursorPosition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cursor = stage.findOne('#cursor');
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.x(cursorPosition?.x);
|
||||||
|
cursor.y(cursorPosition?.y);
|
||||||
|
}, [cursorPosition, stage]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
console.log('obj effect');
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle layer getting deleted and reset
|
||||||
|
for (const l of state.layers) {
|
||||||
|
let layer = stage.findOne(`#${l.id}`) as Konva.Layer | null;
|
||||||
|
if (!layer) {
|
||||||
|
layer = new Konva.Layer({ id: l.id, name: REGIONAL_PROMPT_LAYER_NAME, draggable: true });
|
||||||
|
layer.on('dragmove', (e) => {
|
||||||
|
dispatch(layerTranslated({ layerId: l.id, x: e.target.x(), y: e.target.y() }));
|
||||||
|
});
|
||||||
|
layer.dragBoundFunc(function (pos) {
|
||||||
|
const cursorPos = getScaledCursorPosition(stage);
|
||||||
|
if (!cursorPos) {
|
||||||
|
return this.getAbsolutePosition();
|
||||||
|
}
|
||||||
|
// This prevents the user from dragging the object out of the stage.
|
||||||
|
if (cursorPos.x < 0 || cursorPos.x > stage.width() || cursorPos.y < 0 || cursorPos.y > stage.height()) {
|
||||||
|
return this.getAbsolutePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
});
|
||||||
|
stage.add(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.tool === 'move') {
|
||||||
|
layer.listening(true);
|
||||||
|
} else {
|
||||||
|
layer.listening(l.id === state.selectedLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const o of l.objects) {
|
||||||
|
if (o.kind !== 'line') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let obj = stage.findOne(`#${o.id}`) as Konva.Line | null;
|
||||||
|
if (!obj) {
|
||||||
|
obj = new Konva.Line({
|
||||||
|
id: o.id,
|
||||||
|
key: o.id,
|
||||||
|
strokeWidth: o.strokeWidth,
|
||||||
|
stroke: rgbColorToString(l.color),
|
||||||
|
tension: 0,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round',
|
||||||
|
shadowForStrokeEnabled: false,
|
||||||
|
globalCompositeOperation: o.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
|
listening: false,
|
||||||
|
visible: l.isVisible,
|
||||||
|
});
|
||||||
|
layer.add(obj);
|
||||||
|
}
|
||||||
|
if (obj.points().length < o.points.length) {
|
||||||
|
obj.points(o.points);
|
||||||
|
}
|
||||||
|
if (obj.stroke() !== rgbColorToString(l.color)) {
|
||||||
|
obj.stroke(rgbColorToString(l.color));
|
||||||
|
}
|
||||||
|
if (obj.visible() !== l.isVisible) {
|
||||||
|
obj.visible(l.isVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [dispatch, stage, state.tool, state.layers, state.selectedLayer]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.tool !== 'move') {
|
||||||
|
for (const n of stage.find('.layer-bbox')) {
|
||||||
|
n.visible(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const layer of stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`) as Konva.Layer[]) {
|
||||||
|
const bbox = getKonvaLayerBbox(layer);
|
||||||
|
dispatch(layerBboxChanged({ layerId: layer.id(), bbox }));
|
||||||
|
let rect = layer.findOne('.layer-bbox') as Konva.Rect | null;
|
||||||
|
if (!rect) {
|
||||||
|
rect = new Konva.Rect({
|
||||||
|
id: `${layer.id()}-bbox`,
|
||||||
|
name: 'layer-bbox',
|
||||||
|
strokeWidth: 1,
|
||||||
|
});
|
||||||
|
layer.add(rect);
|
||||||
|
layer.on('mousedown', () => {
|
||||||
|
dispatch(layerSelected(layer.id()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rect.visible(true);
|
||||||
|
rect.x(bbox.x);
|
||||||
|
rect.y(bbox.y);
|
||||||
|
rect.width(bbox.width);
|
||||||
|
rect.height(bbox.height);
|
||||||
|
rect.stroke(state.selectedLayer === layer.id() ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)');
|
||||||
|
}
|
||||||
|
}, [dispatch, stage, state.tool, state.selectedLayer]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $container = atom<HTMLDivElement | null>(null);
|
||||||
|
const containerRef = (el: HTMLDivElement | null) => {
|
||||||
|
$container.set(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StageComponent = () => {
|
||||||
|
const container = useStore($container);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<chakra.div ref={containerRef} tabIndex={-1} sx={{ borderWidth: 1, borderRadius: 'base' }} />
|
||||||
|
<LogicalStage container={container} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,142 @@
|
|||||||
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
|
||||||
|
import { $stage } from 'features/regionalPrompts/components/imperative/konvaApiDraft';
|
||||||
|
import {
|
||||||
|
$cursorPosition,
|
||||||
|
$isMouseDown,
|
||||||
|
$isMouseOver,
|
||||||
|
lineAdded,
|
||||||
|
pointsAdded,
|
||||||
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import type Konva from 'konva';
|
||||||
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
const getTool = () => getStore().getState().regionalPrompts.tool;
|
||||||
|
|
||||||
|
const getIsFocused = (stage: Konva.Stage) => {
|
||||||
|
return stage.container().contains(document.activeElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncCursorPos = (stage: Konva.Stage) => {
|
||||||
|
const pos = getScaledCursorPosition(stage);
|
||||||
|
if (!pos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$cursorPosition.set(pos);
|
||||||
|
return pos;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMouseDown = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onMouseDown = useCallback(
|
||||||
|
(_e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||||
|
const stage = $stage.get();
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pos = syncCursorPos(stage);
|
||||||
|
if (!pos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$isMouseDown.set(true);
|
||||||
|
const tool = getTool();
|
||||||
|
if (tool === 'brush' || tool === 'eraser') {
|
||||||
|
dispatch(lineAdded([pos.x, pos.y]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
return onMouseDown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMouseUp = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onMouseUp = useCallback(
|
||||||
|
(_e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||||
|
const stage = $stage.get();
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tool = getTool();
|
||||||
|
if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) {
|
||||||
|
// Add another point to the last line.
|
||||||
|
$isMouseDown.set(false);
|
||||||
|
const pos = syncCursorPos(stage);
|
||||||
|
if (!pos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(pointsAdded([pos.x, pos.y]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
return onMouseUp;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMouseMove = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onMouseMove = useCallback(
|
||||||
|
(_e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||||
|
const stage = $stage.get();
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pos = syncCursorPos(stage);
|
||||||
|
if (!pos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tool = getTool();
|
||||||
|
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
|
||||||
|
dispatch(pointsAdded([pos.x, pos.y]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
return onMouseMove;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMouseLeave = () => {
|
||||||
|
const onMouseLeave = useCallback((_e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||||
|
const stage = $stage.get();
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$isMouseOver.set(false);
|
||||||
|
$isMouseDown.set(false);
|
||||||
|
$cursorPosition.set(null);
|
||||||
|
}, []);
|
||||||
|
return onMouseLeave;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMouseEnter = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onMouseEnter = useCallback(
|
||||||
|
(e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
const stage = $stage.get();
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$isMouseOver.set(true);
|
||||||
|
const pos = syncCursorPos(stage);
|
||||||
|
if (!pos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!getIsFocused(stage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.evt.buttons !== 1) {
|
||||||
|
$isMouseDown.set(false);
|
||||||
|
} else {
|
||||||
|
$isMouseDown.set(true);
|
||||||
|
const tool = getTool();
|
||||||
|
if (tool === 'brush' || tool === 'eraser') {
|
||||||
|
dispatch(lineAdded([pos.x, pos.y]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
return onMouseEnter;
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import type Konva from 'konva';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Stage } from 'react-konva';
|
||||||
|
|
||||||
|
export const StageWrapper = () => {
|
||||||
|
const [stage, setStage] = useState<Konva.Stage | null>(null);
|
||||||
|
const stageRefCallback = useCallback(
|
||||||
|
(el: Konva.Stage | null) => {
|
||||||
|
setStage(el);
|
||||||
|
},
|
||||||
|
[setStage]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do something with stage
|
||||||
|
}, [stage]);
|
||||||
|
|
||||||
|
return <Stage ref={stageRefCallback} />;
|
||||||
|
};
|
@ -19,7 +19,7 @@ export const getRegionalPromptLayerBlobs = async (
|
|||||||
|
|
||||||
// This automatically omits layers that are not rendered. Rendering is controlled by the layer's `isVisible` flag in redux.
|
// This automatically omits layers that are not rendered. Rendering is controlled by the layer's `isVisible` flag in redux.
|
||||||
const regionalPromptLayers = stage.getLayers().filter((l) => {
|
const regionalPromptLayers = stage.getLayers().filter((l) => {
|
||||||
console.log(l.name(), l.id())
|
console.log(l.name(), l.id());
|
||||||
const isRegionalPromptLayer = l.name() === REGIONAL_PROMPT_LAYER_NAME;
|
const isRegionalPromptLayer = l.name() === REGIONAL_PROMPT_LAYER_NAME;
|
||||||
const isRequestedLayerId = layerIds ? layerIds.includes(l.id()) : true;
|
const isRequestedLayerId = layerIds ? layerIds.includes(l.id()) : true;
|
||||||
return isRegionalPromptLayer && isRequestedLayerId;
|
return isRegionalPromptLayer && isRequestedLayerId;
|
||||||
|
Loading…
Reference in New Issue
Block a user