fix(ui): wip misc regional prompting ui

This commit is contained in:
psychedelicious 2024-04-17 18:11:47 +10:00 committed by Kent Keirsey
parent 20ccdb6c8f
commit aa6bfc8645
5 changed files with 339 additions and 305 deletions

View File

@ -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" />;
};

View File

@ -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]));
}
}
},

View File

@ -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)',
});
};

View File

@ -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}`;

View File

@ -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';