mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): wip misc regional prompting ui
This commit is contained in:
parent
20ccdb6c8f
commit
aa6bfc8645
@ -2,277 +2,32 @@ import { chakra } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
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 { Layer, Tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import {
|
||||
$cursorPosition,
|
||||
BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
BRUSH_PREVIEW_FILL_ID,
|
||||
BRUSH_PREVIEW_LAYER_ID,
|
||||
getPromptRegionLayerBboxId,
|
||||
getPromptRegionLayerObjectGroupId,
|
||||
layerBboxChanged,
|
||||
layerTranslated,
|
||||
REGIONAL_PROMPT_LAYER_BBOX_NAME,
|
||||
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 { KonvaEventObject, Node, NodeConfig } from 'konva/lib/Node';
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import type { Node, NodeConfig } from 'konva/lib/Node';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useLayoutEffect } from 'react';
|
||||
import type { RgbColor } from 'react-colorful';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { useMouseDown, useMouseEnter, useMouseLeave, useMouseMove, useMouseUp } from './mouseEventHooks';
|
||||
import { renderBbox, renderBrushPreview, renderLayers } from './renderers';
|
||||
|
||||
export const $stage = atom<Konva.Stage | null>(null);
|
||||
|
||||
export const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
||||
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
|
||||
|
||||
type Props = {
|
||||
container: HTMLDivElement | null;
|
||||
};
|
||||
|
||||
const renderBrushPreview = (
|
||||
stage: Konva.Stage,
|
||||
tool: Tool,
|
||||
color: RgbColor,
|
||||
cursorPos: Vector2d,
|
||||
brushSize: number
|
||||
) => {
|
||||
// Update the stage's pointer style
|
||||
stage.container().style.cursor = tool === 'move' ? 'default' : 'none';
|
||||
|
||||
// Create the layer if it doesn't exist
|
||||
let layer = stage.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`);
|
||||
if (!layer) {
|
||||
layer = new Konva.Layer({ id: BRUSH_PREVIEW_LAYER_ID, visible: tool !== 'move' });
|
||||
stage.add(layer);
|
||||
}
|
||||
|
||||
// The brush preview is hidden when using the move tool
|
||||
layer.visible(tool !== 'move');
|
||||
|
||||
// Create and/or update the fill circle
|
||||
let fill = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_FILL_ID}`);
|
||||
if (!fill) {
|
||||
fill = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_FILL_ID,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
strokeHitEnabled: false,
|
||||
});
|
||||
layer.add(fill);
|
||||
}
|
||||
fill.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2,
|
||||
fill: rgbColorToString(color),
|
||||
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
});
|
||||
|
||||
// Create and/or update the inner border of the brush preview
|
||||
let borderInner = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_BORDER_INNER_ID}`);
|
||||
if (!borderInner) {
|
||||
borderInner = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
listening: false,
|
||||
stroke: 'rgba(0,0,0,1)',
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
layer.add(borderInner);
|
||||
}
|
||||
borderInner.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
||||
|
||||
// Create and/or update the outer border of the brush preview
|
||||
let borderOuter = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_BORDER_OUTER_ID}`);
|
||||
if (!borderOuter) {
|
||||
borderOuter = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
listening: false,
|
||||
stroke: 'rgba(255,255,255,0.8)',
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
layer.add(borderOuter);
|
||||
}
|
||||
borderOuter.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2 + 1,
|
||||
});
|
||||
};
|
||||
|
||||
export 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}`)) {
|
||||
if (!reduxLayerIds.includes(konvaLayer.id())) {
|
||||
konvaLayer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
for (const reduxLayer of reduxLayers) {
|
||||
let konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
|
||||
|
||||
// New layer - create a new Konva layer
|
||||
if (!konvaLayer) {
|
||||
konvaLayer = new Konva.Layer({
|
||||
id: reduxLayer.id,
|
||||
name: REGIONAL_PROMPT_LAYER_NAME,
|
||||
draggable: true,
|
||||
listening: reduxLayer.id === selectedLayerId,
|
||||
x: reduxLayer.x,
|
||||
y: reduxLayer.y,
|
||||
});
|
||||
if (getOnDragMove) {
|
||||
konvaLayer.on('dragmove', getOnDragMove(reduxLayer.id));
|
||||
}
|
||||
konvaLayer.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(konvaLayer);
|
||||
konvaLayer.add(
|
||||
new Konva.Group({
|
||||
id: getPromptRegionLayerObjectGroupId(reduxLayer.id, uuidv4()),
|
||||
name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
|
||||
listening: false,
|
||||
})
|
||||
);
|
||||
// Brush preview should always be the top layer
|
||||
stage.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)?.moveToTop();
|
||||
} else {
|
||||
konvaLayer.listening(reduxLayer.id === selectedLayerId);
|
||||
konvaLayer.x(reduxLayer.x);
|
||||
konvaLayer.y(reduxLayer.y);
|
||||
}
|
||||
|
||||
const color = rgbColorToString(reduxLayer.color);
|
||||
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`);
|
||||
|
||||
// Remove deleted objects
|
||||
const objectIds = reduxLayer.objects.map((o) => o.id);
|
||||
for (const objectNode of stage.find(`.${reduxLayer.id}-object`)) {
|
||||
if (!objectIds.includes(objectNode.id())) {
|
||||
objectNode.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
for (const reduxObject of reduxLayer.objects) {
|
||||
// TODO: Handle rects, images, etc
|
||||
if (reduxObject.kind !== 'line') {
|
||||
return;
|
||||
}
|
||||
const konvaObject = stage.findOne<Konva.Line>(`#${reduxObject.id}`);
|
||||
|
||||
if (!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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderBbox = (
|
||||
stage: Konva.Stage,
|
||||
tool: Tool,
|
||||
selectedLayerId: string | null,
|
||||
onBboxChanged: (layerId: string, bbox: IRect) => void
|
||||
) => {
|
||||
// Hide all bounding boxes
|
||||
for (const bboxRect of stage.find<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`)) {
|
||||
bboxRect.visible(false);
|
||||
}
|
||||
|
||||
// No selected layer or not using the move tool - nothing more to do here
|
||||
if (!selectedLayerId || tool !== 'move') {
|
||||
return;
|
||||
}
|
||||
|
||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${selectedLayerId}`);
|
||||
assert(konvaLayer, `Selected layer ${selectedLayerId} not found in stage`);
|
||||
|
||||
const bbox = getKonvaLayerBbox(konvaLayer, selectPromptLayerObjectGroup);
|
||||
onBboxChanged(selectedLayerId, bbox);
|
||||
|
||||
let rect = konvaLayer.findOne<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`);
|
||||
if (!rect) {
|
||||
rect = new Konva.Rect({
|
||||
id: getPromptRegionLayerBboxId(selectedLayerId),
|
||||
name: REGIONAL_PROMPT_LAYER_BBOX_NAME,
|
||||
strokeWidth: 1,
|
||||
});
|
||||
konvaLayer.add(rect);
|
||||
}
|
||||
rect.setAttrs({
|
||||
visible: true,
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
stroke: selectedLayerId === selectedLayerId ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)',
|
||||
});
|
||||
};
|
||||
|
||||
const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
return regionalPrompts.layers.find((l) => l.id === regionalPrompts.selectedLayer)?.color;
|
||||
});
|
||||
|
||||
export const LogicalStage = ({ container }: Props) => {
|
||||
export const useStageRenderer = (container: HTMLDivElement | null) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const width = useAppSelector((s) => s.generation.width);
|
||||
const height = useAppSelector((s) => s.generation.height);
|
||||
@ -286,12 +41,19 @@ export const LogicalStage = ({ container }: Props) => {
|
||||
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]);
|
||||
const onLayerPosChanged = useCallback(
|
||||
(layerId: string, x: number, y: number) => {
|
||||
dispatch(layerTranslated({ layerId, x, y }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onBboxChanged = useCallback(
|
||||
(layerId: string, bbox: IRect) => {
|
||||
dispatch(layerBboxChanged({ layerId, bbox }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
console.log('Initializing stage');
|
||||
@ -339,37 +101,28 @@ export const LogicalStage = ({ container }: Props) => {
|
||||
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]
|
||||
);
|
||||
|
||||
const onBboxChanged = useCallback(
|
||||
(layerId: string, bbox: IRect) => {
|
||||
dispatch(layerBboxChanged({ layerId, bbox }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
useLayoutEffect(() => {
|
||||
if (!stage || !cursorPosition || !selectedLayerColor) {
|
||||
return;
|
||||
}
|
||||
renderBrushPreview(stage, state.tool, selectedLayerColor, cursorPosition, state.brushSize);
|
||||
}, [stage, state.tool, cursorPosition, state.brushSize, selectedLayerColor]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
console.log('Rendering layers');
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
renderLayers(stage, state.layers, state.selectedLayer, getOnDragMove);
|
||||
}, [getOnDragMove, stage, state.layers, state.selectedLayer]);
|
||||
renderLayers(stage, state.layers, state.selectedLayer, onLayerPosChanged);
|
||||
}, [onLayerPosChanged, stage, state.layers, state.selectedLayer]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
console.log('bbox effect');
|
||||
console.log('Rendering bbox');
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
renderBbox(stage, state.tool, state.selectedLayer, onBboxChanged);
|
||||
}, [dispatch, stage, state.tool, state.selectedLayer, onBboxChanged]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const $container = atom<HTMLDivElement | null>(null);
|
||||
@ -379,10 +132,6 @@ const containerRef = (el: HTMLDivElement | null) => {
|
||||
|
||||
export const StageComponent = () => {
|
||||
const container = useStore($container);
|
||||
return (
|
||||
<>
|
||||
<chakra.div ref={containerRef} tabIndex={-1} borderWidth={1} borderRadius="base" h="min-content" />
|
||||
<LogicalStage container={container} />
|
||||
</>
|
||||
);
|
||||
useStageRenderer(container);
|
||||
return <chakra.div ref={containerRef} tabIndex={-1} borderWidth={1} borderRadius="base" h="min-content" />;
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ export const useMouseDown = () => {
|
||||
$isMouseDown.set(true);
|
||||
const tool = getTool();
|
||||
if (tool === 'brush' || tool === 'eraser') {
|
||||
dispatch(lineAdded([pos.x, pos.y]));
|
||||
dispatch(lineAdded([pos.x, pos.y, pos.x, pos.y]));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
@ -51,26 +51,17 @@ export const useMouseDown = () => {
|
||||
};
|
||||
|
||||
export const useMouseUp = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const onMouseUp = useCallback(
|
||||
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||
const stage = e.target.getStage();
|
||||
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]
|
||||
);
|
||||
const onMouseUp = useCallback((e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
const tool = getTool();
|
||||
if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) {
|
||||
// Add another point to the last line.
|
||||
$isMouseDown.set(false);
|
||||
}
|
||||
}, []);
|
||||
return onMouseUp;
|
||||
};
|
||||
|
||||
@ -131,7 +122,7 @@ export const useMouseEnter = () => {
|
||||
$isMouseDown.set(true);
|
||||
const tool = getTool();
|
||||
if (tool === 'brush' || tool === 'eraser') {
|
||||
dispatch(lineAdded([pos.x, pos.y]));
|
||||
dispatch(lineAdded([pos.x, pos.y, pos.x, pos.y]));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -0,0 +1,288 @@
|
||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
|
||||
import type { Layer, Tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import {
|
||||
BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
BRUSH_PREVIEW_FILL_ID,
|
||||
BRUSH_PREVIEW_LAYER_ID,
|
||||
getPromptRegionLayerBboxId,
|
||||
getPromptRegionLayerObjectGroupId,
|
||||
REGIONAL_PROMPT_LAYER_BBOX_NAME,
|
||||
REGIONAL_PROMPT_LAYER_LINE_NAME,
|
||||
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 { IRect, Vector2d } from 'konva/lib/types';
|
||||
import type { RgbColor } from 'react-colorful';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { selectPromptLayerObjectGroup } from './konvaApiDraft';
|
||||
|
||||
/**
|
||||
* Renders the brush preview for the selected tool.
|
||||
* @param stage The konva stage to render on.
|
||||
* @param tool The selected tool.
|
||||
* @param color The selected layer's color.
|
||||
* @param cursorPos The cursor position.
|
||||
* @param brushSize The brush size.
|
||||
*/
|
||||
export const renderBrushPreview = (
|
||||
stage: Konva.Stage,
|
||||
tool: Tool,
|
||||
color: RgbColor,
|
||||
cursorPos: Vector2d,
|
||||
brushSize: number
|
||||
) => {
|
||||
// Update the stage's pointer style
|
||||
stage.container().style.cursor = tool === 'move' ? 'default' : 'none';
|
||||
|
||||
// Create the layer if it doesn't exist
|
||||
let layer = stage.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`);
|
||||
if (!layer) {
|
||||
// Initialize the brush preview layer & add to the stage
|
||||
layer = new Konva.Layer({ id: BRUSH_PREVIEW_LAYER_ID, visible: tool !== 'move' });
|
||||
stage.add(layer);
|
||||
// The brush preview is hidden and shown as the mouse leaves and enters the stage
|
||||
stage.on('mouseleave', (e) => {
|
||||
e.target.getStage()?.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)?.visible(false);
|
||||
});
|
||||
stage.on('mouseenter', (e) => {
|
||||
e.target.getStage()?.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)?.visible(true);
|
||||
});
|
||||
}
|
||||
|
||||
// The brush preview is hidden when using the move tool
|
||||
layer.visible(tool !== 'move');
|
||||
|
||||
// Create and/or update the fill circle
|
||||
let fill = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_FILL_ID}`);
|
||||
if (!fill) {
|
||||
fill = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_FILL_ID,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
});
|
||||
layer.add(fill);
|
||||
}
|
||||
fill.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2,
|
||||
fill: rgbColorToString(color),
|
||||
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
});
|
||||
|
||||
// Create and/or update the inner border of the brush preview
|
||||
let borderInner = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_BORDER_INNER_ID}`);
|
||||
if (!borderInner) {
|
||||
borderInner = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
listening: false,
|
||||
stroke: 'rgba(0,0,0,1)',
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
layer.add(borderInner);
|
||||
}
|
||||
borderInner.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
||||
|
||||
// Create and/or update the outer border of the brush preview
|
||||
let borderOuter = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_BORDER_OUTER_ID}`);
|
||||
if (!borderOuter) {
|
||||
borderOuter = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
listening: false,
|
||||
stroke: 'rgba(255,255,255,0.8)',
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
layer.add(borderOuter);
|
||||
}
|
||||
borderOuter.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2 + 1,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the layers on the stage.
|
||||
* @param stage The konva stage to render on.
|
||||
* @param reduxLayers Array of the layers from the redux store.
|
||||
* @param selectedLayerId The selected layer id.
|
||||
* @param onLayerPosChanged Callback for when the layer's position changes. This is optional to allow for offscreen rendering.
|
||||
* @returns
|
||||
*/
|
||||
export const renderLayers = (
|
||||
stage: Konva.Stage,
|
||||
reduxLayers: Layer[],
|
||||
selectedLayerId: string | null,
|
||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||
) => {
|
||||
const reduxLayerIds = reduxLayers.map((l) => l.id);
|
||||
|
||||
// Remove un-rendered layers
|
||||
for (const konvaLayer of stage.find<Konva.Layer>(`.${REGIONAL_PROMPT_LAYER_NAME}`)) {
|
||||
if (!reduxLayerIds.includes(konvaLayer.id())) {
|
||||
konvaLayer.destroy();
|
||||
console.log(`Destroyed layer ${konvaLayer.id()}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const reduxLayer of reduxLayers) {
|
||||
let konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
|
||||
|
||||
if (!konvaLayer) {
|
||||
// This layer hasn't been added to the konva state yet
|
||||
konvaLayer = new Konva.Layer({
|
||||
id: reduxLayer.id,
|
||||
name: REGIONAL_PROMPT_LAYER_NAME,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// Create a `dragmove` listener for this layer
|
||||
if (onLayerPosChanged) {
|
||||
konvaLayer.on('dragend', function (e) {
|
||||
onLayerPosChanged(reduxLayer.id, e.target.x(), e.target.y());
|
||||
});
|
||||
}
|
||||
|
||||
// The dragBoundFunc limits how far the layer can be dragged
|
||||
konvaLayer.dragBoundFunc(function (pos) {
|
||||
const cursorPos = getScaledCursorPosition(stage);
|
||||
if (!cursorPos) {
|
||||
return this.getAbsolutePosition();
|
||||
}
|
||||
// Prevent the user from dragging the layer out of the stage bounds.
|
||||
if (cursorPos.x < 0 || cursorPos.x > stage.width() || cursorPos.y < 0 || cursorPos.y > stage.height()) {
|
||||
return this.getAbsolutePosition();
|
||||
}
|
||||
return pos;
|
||||
});
|
||||
|
||||
// The object group holds all of the layer's objects (e.g. lines and rects)
|
||||
const konvaObjectGroup = new Konva.Group({
|
||||
id: getPromptRegionLayerObjectGroupId(reduxLayer.id, uuidv4()),
|
||||
name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
|
||||
listening: false,
|
||||
});
|
||||
konvaLayer.add(konvaObjectGroup);
|
||||
|
||||
stage.add(konvaLayer);
|
||||
|
||||
// When a layer is added, it ends up on top of the brush preview - we need to move the preview back to the top.
|
||||
stage.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)?.moveToTop();
|
||||
}
|
||||
|
||||
// Update the layer's position and listening state (only the selected layer is listening)
|
||||
konvaLayer.setAttrs({
|
||||
listening: reduxLayer.id === selectedLayerId,
|
||||
x: reduxLayer.x,
|
||||
y: reduxLayer.y,
|
||||
});
|
||||
|
||||
const color = rgbColorToString(reduxLayer.color);
|
||||
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`);
|
||||
assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`);
|
||||
|
||||
// Remove deleted objects
|
||||
const objectIds = reduxLayer.objects.map((o) => o.id);
|
||||
for (const objectNode of konvaLayer.find(`.${REGIONAL_PROMPT_LAYER_LINE_NAME}`)) {
|
||||
if (!objectIds.includes(objectNode.id())) {
|
||||
objectNode.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
for (const reduxObject of reduxLayer.objects) {
|
||||
// TODO: Handle rects, images, etc
|
||||
if (reduxObject.kind !== 'line') {
|
||||
return;
|
||||
}
|
||||
|
||||
let konvaObject = stage.findOne<Konva.Line>(`#${reduxObject.id}`);
|
||||
|
||||
if (!konvaObject) {
|
||||
// This object hasn't been added to the konva state yet.
|
||||
konvaObject = new Konva.Line({
|
||||
id: reduxObject.id,
|
||||
key: reduxObject.id,
|
||||
name: REGIONAL_PROMPT_LAYER_LINE_NAME,
|
||||
strokeWidth: reduxObject.strokeWidth,
|
||||
tension: 0,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
listening: false,
|
||||
});
|
||||
konvaObjectGroup.add(konvaObject);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param stage The konva stage to render on.
|
||||
* @param tool The current tool.
|
||||
* @param selectedLayerId The currently selected layer id.
|
||||
* @param onBboxChanged A callback to be called when the bounding box changes.
|
||||
* @returns
|
||||
*/
|
||||
export const renderBbox = (
|
||||
stage: Konva.Stage,
|
||||
tool: Tool,
|
||||
selectedLayerId: string | null,
|
||||
onBboxChanged: (layerId: string, bbox: IRect) => void
|
||||
) => {
|
||||
// Hide all bounding boxes
|
||||
for (const bboxRect of stage.find<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`)) {
|
||||
bboxRect.visible(false);
|
||||
}
|
||||
|
||||
// No selected layer or not using the move tool - nothing more to do here
|
||||
if (!selectedLayerId || tool !== 'move') {
|
||||
return;
|
||||
}
|
||||
|
||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${selectedLayerId}`);
|
||||
assert(konvaLayer, `Selected layer ${selectedLayerId} not found in stage`);
|
||||
|
||||
const bbox = getKonvaLayerBbox(konvaLayer, selectPromptLayerObjectGroup);
|
||||
onBboxChanged(selectedLayerId, bbox);
|
||||
|
||||
let rect = konvaLayer.findOne<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`);
|
||||
if (!rect) {
|
||||
rect = new Konva.Rect({
|
||||
id: getPromptRegionLayerBboxId(selectedLayerId),
|
||||
name: REGIONAL_PROMPT_LAYER_BBOX_NAME,
|
||||
strokeWidth: 1,
|
||||
});
|
||||
konvaLayer.add(rect);
|
||||
}
|
||||
rect.setAttrs({
|
||||
visible: true,
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
stroke: selectedLayerId === selectedLayerId ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)',
|
||||
});
|
||||
};
|
@ -180,7 +180,7 @@ export const regionalPromptsSlice = createSlice({
|
||||
layer.color = color;
|
||||
},
|
||||
lineAdded: {
|
||||
reducer: (state, action: PayloadAction<[number, number], string, { uuid: string }>) => {
|
||||
reducer: (state, action: PayloadAction<[number, number, number, number], string, { uuid: string }>) => {
|
||||
const layer = state.layers.find((l) => l.id === state.selectedLayer);
|
||||
if (!layer || layer.kind !== 'promptRegionLayer') {
|
||||
return;
|
||||
@ -190,11 +190,16 @@ export const regionalPromptsSlice = createSlice({
|
||||
kind: 'line',
|
||||
tool: state.tool,
|
||||
id: lineId,
|
||||
points: [action.payload[0] - layer.x, action.payload[1] - layer.y],
|
||||
points: [
|
||||
action.payload[0] - layer.x,
|
||||
action.payload[1] - layer.y,
|
||||
action.payload[2] - layer.x,
|
||||
action.payload[3] - layer.y,
|
||||
],
|
||||
strokeWidth: state.brushSize,
|
||||
});
|
||||
},
|
||||
prepare: (payload: [number, number]) => ({ payload, meta: { uuid: uuidv4() } }),
|
||||
prepare: (payload: [number, number, number, number]) => ({ payload, meta: { uuid: uuidv4() } }),
|
||||
},
|
||||
pointsAdded: (state, action: PayloadAction<[number, number]>) => {
|
||||
const layer = state.layers.find((l) => l.id === state.selectedLayer);
|
||||
@ -293,6 +298,7 @@ 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_LINE_NAME = 'regionalPromptLayerLine';
|
||||
export const REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME = 'regionalPromptLayerObjectGroup';
|
||||
export const REGIONAL_PROMPT_LAYER_BBOX_NAME = 'regionalPromptLayerBbox';
|
||||
export const getPromptRegionLayerId = (layerId: string) => `layer_${layerId}`;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||
import { renderLayers } from 'features/regionalPrompts/components/imperative/konvaApiDraft';
|
||||
import { renderLayers } from 'features/regionalPrompts/components/imperative/renderers';
|
||||
import { REGIONAL_PROMPT_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import Konva from 'konva';
|
||||
import { assert } from 'tsafe';
|
||||
|
Loading…
Reference in New Issue
Block a user