diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx index a8ada1c1c4..4938c7d29c 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx @@ -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(null); export const selectPromptLayerObjectGroup = (item: Node) => 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(`#${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(`#${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(`#${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(`#${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) => void -) => { - const reduxLayerIds = reduxLayers.map((l) => l.id); - - // Remove deleted layers - we know these are of type Layer - for (const konvaLayer of stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`)) { - if (!reduxLayerIds.includes(konvaLayer.id())) { - konvaLayer.destroy(); - } - } - - for (const reduxLayer of reduxLayers) { - let konvaLayer = stage.findOne(`#${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(`#${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(`.${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(`#${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(`.${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(`#${selectedLayerId}`); - assert(konvaLayer, `Selected layer ${selectedLayerId} not found in stage`); - - const bbox = getKonvaLayerBbox(konvaLayer, selectPromptLayerObjectGroup); - onBboxChanged(selectedLayerId, bbox); - - let rect = konvaLayer.findOne(`.${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) => { - 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(null); @@ -379,10 +132,6 @@ const containerRef = (el: HTMLDivElement | null) => { export const StageComponent = () => { const container = useStore($container); - return ( - <> - - - - ); + useStageRenderer(container); + return ; }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/mouseEventHooks.ts b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/mouseEventHooks.ts index 4e965cdcec..ec16da579d 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/mouseEventHooks.ts @@ -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) => { - 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) => { + 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])); } } }, diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/renderers.ts b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/renderers.ts new file mode 100644 index 0000000000..f6163faeb4 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/renderers.ts @@ -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(`#${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(`#${BRUSH_PREVIEW_LAYER_ID}`)?.visible(false); + }); + stage.on('mouseenter', (e) => { + e.target.getStage()?.findOne(`#${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(`#${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(`#${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(`#${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(`.${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(`#${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(`#${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(`.${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(`#${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(`.${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(`#${selectedLayerId}`); + assert(konvaLayer, `Selected layer ${selectedLayerId} not found in stage`); + + const bbox = getKonvaLayerBbox(konvaLayer, selectPromptLayerObjectGroup); + onBboxChanged(selectedLayerId, bbox); + + let rect = konvaLayer.findOne(`.${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)', + }); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts index e99d759456..f760ec9e91 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -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}`; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts index 588a188506..ce6f751479 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts @@ -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';