feat(ui): abstract layer renderer

This commit is contained in:
psychedelicious 2024-04-17 13:07:54 +10:00 committed by Kent Keirsey
parent d34e431002
commit 1f8f429d55
2 changed files with 151 additions and 146 deletions

View File

@ -1,10 +1,10 @@
import { chakra } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { getStore } from 'app/store/nanostores/store';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'features/canvas/util/colorToString';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import type { Tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
import type { Layer, Tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
import {
$cursorPosition,
BRUSH_PREVIEW_BORDER_INNER_ID,
@ -17,13 +17,14 @@ import {
layerTranslated,
REGIONAL_PROMPT_LAYER_NAME,
REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
selectRegionalPromptsSlice,
} 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 { KonvaEventObject, Node, NodeConfig } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types';
import { atom } from 'nanostores';
import { useLayoutEffect } from 'react';
import { useCallback, useLayoutEffect } from 'react';
import type { RgbColor } from 'react-colorful';
import { v4 as uuidv4 } from 'uuid';
@ -43,7 +44,7 @@ const isKonvaLine = (node: Node<NodeConfig>): node is Konva.Line => node.nodeTyp
const isKonvaGroup = (node: Node<NodeConfig>): node is Konva.Group => node.nodeType === 'Group';
const isKonvaRect = (node: Node<NodeConfig>): node is Konva.Rect => node.nodeType === 'Rect';
const brushPreviewHandler = (
const renderBrushPreview = (
stage: Konva.Stage,
tool: Tool,
color: RgbColor,
@ -112,89 +113,13 @@ const brushPreviewHandler = (
});
};
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(() => {
if (!stage || !cursorPosition) {
return;
}
const color = getStore()
.getState()
.regionalPrompts.layers.find((l) => l.id === state.selectedLayer)?.color;
if (!color) {
return;
}
brushPreviewHandler(stage, state.tool, color, cursorPosition, state.brushSize);
}, [stage, state.tool, cursorPosition, state.brushSize, state.selectedLayer]);
useLayoutEffect(() => {
console.log('init effect');
if (!props.container) {
return;
}
const stage = new Konva.Stage({
container: props.container,
});
$stage.set(stage);
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('obj effect');
if (!stage) {
return;
}
const reduxLayerIds = state.layers.map((l) => l.id);
const renderLayers = (
stage: Konva.Stage,
reduxLayers: Layer[],
selectedLayerId: string | null,
getOnDragMove?: (layerId: string) => (e: KonvaEventObject<MouseEvent>) => void
) => {
const reduxLayerIds = reduxLayers.map((l) => l.id);
// Remove deleted layers - we know these are of type Layer
for (const konvaLayer of stage.find<Konva.Layer>(`.${REGIONAL_PROMPT_LAYER_NAME}`)) {
@ -203,7 +128,7 @@ export const LogicalStage = (props: Props) => {
}
}
for (const reduxLayer of state.layers) {
for (const reduxLayer of reduxLayers) {
let konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
// New layer - create a new Konva layer
@ -212,19 +137,13 @@ export const LogicalStage = (props: Props) => {
id: reduxLayer.id,
name: REGIONAL_PROMPT_LAYER_NAME,
draggable: true,
listening: reduxLayer.id === state.selectedLayer,
listening: reduxLayer.id === selectedLayerId,
x: reduxLayer.x,
y: reduxLayer.y,
});
konvaLayer.on('dragmove', function (e) {
dispatch(
layerTranslated({
layerId: reduxLayer.id,
x: e.target.x(),
y: e.target.y(),
})
);
});
if (getOnDragMove) {
konvaLayer.on('dragmove', getOnDragMove(reduxLayer.id));
}
konvaLayer.dragBoundFunc(function (pos) {
const cursorPos = getScaledCursorPosition(stage);
if (!cursorPos) {
@ -248,7 +167,7 @@ export const LogicalStage = (props: Props) => {
// Brush preview should always be the top layer
stage.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)?.moveToTop();
} else {
konvaLayer.listening(reduxLayer.id === state.selectedLayer);
konvaLayer.listening(reduxLayer.id === selectedLayerId);
konvaLayer.x(reduxLayer.x);
konvaLayer.y(reduxLayer.y);
}
@ -306,7 +225,92 @@ export const LogicalStage = (props: Props) => {
}
}
}
}, [dispatch, stage, state.tool, state.layers, state.selectedLayer]);
};
const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
return regionalPrompts.layers.find((l) => l.id === regionalPrompts.selectedLayer)?.color;
});
export const LogicalStage = ({ container }: 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);
const selectedLayerColor = useAppSelector(selectSelectedLayerColor);
useLayoutEffect(() => {
if (!stage || !cursorPosition || !selectedLayerColor) {
return;
}
renderBrushPreview(stage, state.tool, selectedLayerColor, cursorPosition, state.brushSize);
}, [stage, state.tool, cursorPosition, state.brushSize, selectedLayerColor]);
useLayoutEffect(() => {
console.log('init effect');
if (!container) {
return;
}
$stage.set(
new Konva.Stage({
container,
})
);
return () => {
$stage.get()?.destroy();
};
}, [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) {
return;
}
stage.width(width);
stage.height(height);
}, [stage, width, height]);
const getOnDragMove = useCallback(
(layerId: string) => (e: KonvaEventObject<MouseEvent>) => {
dispatch(layerTranslated({ layerId, x: e.target.x(), y: e.target.y() }));
},
[dispatch]
);
useLayoutEffect(() => {
console.log('obj effect');
if (!stage) {
return;
}
renderLayers(stage, state.layers, state.selectedLayer, getOnDragMove);
}, [getOnDragMove, stage, state.layers, state.selectedLayer]);
useLayoutEffect(() => {
if (!stage) {

View File

@ -56,7 +56,7 @@ type PromptRegionLayer = LayerBase & {
color: RgbColor;
};
type Layer = PromptRegionLayer;
export type Layer = PromptRegionLayer;
type RegionalPromptsState = {
_version: 1;
@ -291,4 +291,5 @@ export const REGIONAL_PROMPT_LAYER_NAME = 'regionalPromptLayer';
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}`;
export const getPromptRegionLayerObjectGroupId = (layerId: string, groupId: string) =>
`${layerId}.objectGroup_${groupId}`;