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

View File

@ -56,7 +56,7 @@ type PromptRegionLayer = LayerBase & {
color: RgbColor; color: RgbColor;
}; };
type Layer = PromptRegionLayer; export type Layer = PromptRegionLayer;
type RegionalPromptsState = { type RegionalPromptsState = {
_version: 1; _version: 1;
@ -291,4 +291,5 @@ 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 getPromptRegionLayerId = (layerId: string) => `layer_${layerId}`;
export const getPromptRegionLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; 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}`;