diff --git a/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts b/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts index 792b6d38e4..01c164d439 100644 --- a/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts +++ b/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts @@ -8,3 +8,7 @@ export const roundDownToMultipleMin = (num: number, multiple: number): number => export const roundToMultiple = (num: number, multiple: number): number => { return Math.round(num / multiple) * multiple; }; + +export const roundToMultipleMin = (num: number, multiple: number): number => { + return Math.max(Math.round(num / multiple) * multiple, multiple); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx new file mode 100644 index 0000000000..f36898e10c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -0,0 +1,40 @@ +import { Box, Flex, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { $stageScale } from 'features/controlLayers/store/controlLayersSlice'; +import { round } from 'lodash-es'; +import { memo } from 'react'; + +export const HeadsUpDisplay = memo(() => { + const stageScale = useStore($stageScale); + const layerCount = useAppSelector((s) => s.controlLayers.present.layers.length); + const bbox = useAppSelector((s) => s.controlLayers.present.bbox); + + return ( + + + + + + + + + + + ); +}); + +HeadsUpDisplay.displayName = 'HeadsUpDisplay'; + +const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => { + return ( + + {label}: + + {value} + + + ); +}); + +HUDItem.displayName = 'HUDItem'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ab64063e15..0e089104d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,9 +1,10 @@ -import { Box, Flex, Heading } from '@invoke-ai/ui-library'; +import { $alt, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, @@ -12,12 +13,11 @@ import { } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; -import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { + $bbox, $brushColor, $brushSize, $brushSpacingPx, - $genBbox, $isDrawing, $isMouseDown, $isSpaceDown, @@ -100,10 +100,6 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, () => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX), [state.brushSize] ); - const bbox = useMemo( - () => ({ x: state.x, y: state.y, width: state.size.width, height: state.size.height }), - [state.x, state.y, state.size.width, state.size.height] - ); useLayoutEffect(() => { $brushColor.set(brushColor); @@ -111,7 +107,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, $brushSpacingPx.set(brushSpacingPx); $selectedLayer.set(selectedLayer); $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); - $genBbox.set(bbox); + $bbox.set(state.bbox); }, [ brushSpacingPx, brushColor, @@ -120,7 +116,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, state.brushSize, state.selectedLayerId, state.brushColor, - bbox, + state.bbox, ]); const onLayerPosChanged = useCallback( @@ -257,7 +253,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, return; } log.trace('Rendering tool preview'); - renderers.renderPreviewLayer( + renderers.renderToolPreview( stage, tool, brushColor, @@ -267,41 +263,36 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, lastMouseDownPos, state.brushSize, isDrawing, - isMouseDown, - $genBbox, - onBboxTransformed + isMouseDown ); - renderImageDimsPreview(stage, bbox, tool); }, [ asPreview, - stage, - tool, brushColor, - selectedLayer, - state.globalMaskLayerOpacity, - lastCursorPos, - lastMouseDownPos, - state.brushSize, - renderers, isDrawing, isMouseDown, - bbox, - stageScale, - onBboxTransformed, + lastCursorPos, + lastMouseDownPos, + renderers, + selectedLayer?.type, + stage, + state.brushSize, + state.globalMaskLayerOpacity, + tool, ]); + useLayoutEffect(() => { + if (asPreview) { + // Preview should not display tool + return; + } + log.trace('Rendering bbox preview'); + renderers.renderBboxPreview(stage, state.bbox, tool, $bbox.get, onBboxTransformed, $shift.get, $meta.get, $alt.get); + }, [asPreview, onBboxTransformed, renderers, stage, state.bbox, tool]); + useLayoutEffect(() => { log.trace('Rendering layers'); - renderers.renderLayers( - stage, - bbox, - state.layers, - state.globalMaskLayerOpacity, - tool, - getImageDTO, - onLayerPosChanged - ); - }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers, bbox]); + renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged); + }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]); useLayoutEffect(() => { if (asPreview) { @@ -369,6 +360,11 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { overflow="hidden" data-testid="control-layers-canvas" /> + {!asPreview && ( + + + + )} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index dfc8a15f7c..feb91867ea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -2,7 +2,6 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; /** @@ -52,7 +51,6 @@ const updateCALayerImageSource = async ( stage: Konva.Stage, konvaLayer: Konva.Layer, layerState: ControlAdapterLayer, - bbox: IRect, getImageDTO: (imageName: string) => Promise ): Promise => { const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; @@ -74,7 +72,7 @@ const updateCALayerImageSource = async ( id: imageId, image: imageEl, }); - updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); + updateCALayerImageAttrs(stage, konvaImage, layerState); // Must cache after this to apply the filters konvaImage.cache(); imageEl.id = imageId; @@ -95,8 +93,7 @@ const updateCALayerImageSource = async ( const updateCALayerImageAttrs = ( stage: Konva.Stage, konvaImage: Konva.Image, - layerState: ControlAdapterLayer, - bbox: IRect + layerState: ControlAdapterLayer ): void => { let needsCache = false; // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, @@ -104,10 +101,8 @@ const updateCALayerImageAttrs = ( // TODO(psyche): Investigate and report upstream. const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; if ( - konvaImage.x() !== bbox.x || - konvaImage.y() !== bbox.y || - konvaImage.width() !== bbox.width || - konvaImage.height() !== bbox.height || + konvaImage.x() !== layerState.x || + konvaImage.y() !== layerState.y || konvaImage.visible() !== layerState.isEnabled || hasFilter !== layerState.isFilterEnabled ) { @@ -115,7 +110,6 @@ const updateCALayerImageAttrs = ( opacity: layerState.opacity, scaleX: 1, scaleY: 1, - ...bbox, visible: layerState.isEnabled, filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], }); @@ -139,7 +133,6 @@ const updateCALayerImageAttrs = ( export const renderCALayer = ( stage: Konva.Stage, layerState: ControlAdapterLayer, - bbox: IRect, zIndex: number, getImageDTO: (imageName: string) => Promise ): void => { @@ -164,8 +157,8 @@ export const renderCALayer = ( } if (imageSourceNeedsUpdate) { - updateCALayerImageSource(stage, konvaLayer, layerState, bbox, getImageDTO); + updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); } else if (konvaImage) { - updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); + updateCALayerImageAttrs(stage, konvaImage, layerState); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index a2e7f4735c..18e893fb24 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -3,7 +3,7 @@ import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; -import { renderPreviewLayer } from 'features/controlLayers/konva/renderers/previewLayer'; +import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; @@ -16,7 +16,6 @@ import { isRenderableLayer, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; import { debounce } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; @@ -35,7 +34,6 @@ import type { ImageDTO } from 'services/api/types'; */ const renderLayers = ( stage: Konva.Stage, - bbox: IRect, layerStates: Layer[], globalMaskLayerOpacity: number, tool: Tool, @@ -56,7 +54,7 @@ const renderLayers = ( renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); } if (isControlAdapterLayer(layer)) { - renderCALayer(stage, layer, bbox, zIndex, getImageDTO); + renderCALayer(stage, layer, zIndex, getImageDTO); } if (isInitialImageLayer(layer)) { renderIILayer(stage, layer, zIndex, getImageDTO); @@ -76,7 +74,8 @@ const renderLayers = ( * All the renderers for the Konva stage. */ export const renderers = { - renderPreviewLayer, + renderToolPreview, + renderBboxPreview, renderLayers, updateBboxes, }; @@ -87,7 +86,8 @@ export const renderers = { * @returns The renderers with debouncing applied */ const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ - renderPreviewLayer: debounce(renderPreviewLayer, ms), + renderToolPreview: debounce(renderToolPreview, ms), + renderBboxPreview: debounce(renderBboxPreview, ms), renderLayers: debounce(renderLayers, ms), updateBboxes: debounce(updateBboxes, ms), }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index 2b5a0e6f3f..36c6d6c8a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -1,4 +1,4 @@ -import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { BBOX_SELECTED_STROKE, @@ -21,18 +21,13 @@ import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/k import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; -import type { WritableAtom } from 'nanostores'; import { assert } from 'tsafe'; /** * Creates the singleton preview layer and all its objects. * @param stage The konva stage */ -const getPreviewLayer = ( - stage: Konva.Stage, - $genBbox: WritableAtom, - onBboxTransformed: (bbox: IRect) => void -): Konva.Layer => { +const getPreviewLayer = (stage: Konva.Stage): Konva.Layer => { let previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); if (previewLayer) { return previewLayer; @@ -40,6 +35,168 @@ const getPreviewLayer = ( // Initialize the preview layer & add to the stage previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); stage.add(previewLayer); + return previewLayer; +}; + +export const getBboxPreviewGroup = ( + stage: Konva.Stage, + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean +): Konva.Group => { + const previewLayer = getPreviewLayer(stage); + let bboxPreviewGroup = previewLayer.findOne(`#${PREVIEW_GENERATION_BBOX_GROUP}`); + + if (bboxPreviewGroup) { + return bboxPreviewGroup; + } + console.log('creating new bbox'); + + // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully + // transparent rect for this purpose. + bboxPreviewGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP }); + const bboxRect = new Konva.Rect({ + id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, + listening: true, + strokeEnabled: false, + draggable: true, + fill: 'rgba(255,0,0,0.3)', + ...getBbox(), + }); + bboxRect.on('dragmove', () => { + const gridSize = getMetaKey() ? 8 : 64; + const oldBbox = getBbox(); + const newBbox: IRect = { + ...oldBbox, + x: roundToMultiple(bboxRect.x(), gridSize), + y: roundToMultiple(bboxRect.y(), gridSize), + }; + bboxRect.setAttrs(newBbox); + if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { + onBboxTransformed(newBbox); + } + }); + const bboxTransformer = new Konva.Transformer({ + id: PREVIEW_GENERATION_BBOX_TRANSFORMER, + borderDash: [5, 5], + borderStroke: 'rgba(212,216,234,1)', + borderEnabled: true, + rotateEnabled: false, + keepRatio: false, + ignoreStroke: true, + listening: false, + flipEnabled: false, + anchorFill: 'rgba(212,216,234,1)', + anchorStroke: 'rgb(42,42,42)', + anchorSize: 12, + anchorCornerRadius: 3, + // shiftBehavior: 'none', + centeredScaling: false, + anchorStyleFunc: (anchor) => { + // Make the x/y resize anchors little bars + if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { + anchor.height(8); + anchor.offsetY(4); + anchor.width(30); + anchor.offsetX(15); + } + if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { + anchor.height(30); + anchor.offsetY(15); + anchor.width(8); + anchor.offsetX(4); + } + }, + anchorDragBoundFunc: (oldAbsPos, newAbsPos) => { + const gridSize = getMetaKey() ? 8 : 64; + const scaledGridSize = gridSize * stage.scaleX(); + // Calculate the offset of the grid. + const stageAbsPos = stage.getAbsolutePosition(); + const offsetX = stageAbsPos.x % scaledGridSize; + const offsetY = stageAbsPos.y % scaledGridSize; + const finalPos = { + x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, + y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, + }; + console.log('scaledGridSize', scaledGridSize); + console.log('offsetX', offsetX); + console.log('offsetY', offsetY); + console.log('newAbsPosX', newAbsPos.x); + console.log('newAbsPosY', newAbsPos.y); + console.log('finalPos', finalPos); + console.log('finalPosScaled', { x: finalPos.x * stage.scaleX(), y: finalPos.y * stage.scaleY() }); + + return finalPos; + }, + }); + + bboxTransformer.on('transform', () => { + let gridSize = getMetaKey() ? 8 : 64; + + if (getAltKey()) { + gridSize = gridSize * 2; + } + + const bbox = { + x: Math.round(bboxRect.x()), + y: Math.round(bboxRect.y()), + width: roundToMultipleMin(bboxRect.width() * bboxRect.scaleX(), gridSize), + height: roundToMultipleMin(bboxRect.height() * bboxRect.scaleY(), gridSize), + }; + bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + onBboxTransformed(bbox); + }); + + // The transformer will always be transforming the dummy rect + bboxTransformer.nodes([bboxRect]); + bboxPreviewGroup.add(bboxRect); + bboxPreviewGroup.add(bboxTransformer); + previewLayer.add(bboxPreviewGroup); + return bboxPreviewGroup; +}; + +const ALL_ANCHORS: string[] = [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', +]; +const NO_ANCHORS: string[] = []; + +export const renderBboxPreview = ( + stage: Konva.Stage, + bbox: IRect, + tool: Tool, + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean +): void => { + const bboxGroup = getBboxPreviewGroup(stage, getBbox, onBboxTransformed, getShiftKey, getMetaKey, getAltKey); + const bboxRect = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); + const bboxTransformer = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); + bboxRect?.setAttrs({ ...bbox, listening: tool === 'move' }); + bboxTransformer?.setAttrs({ + listening: tool === 'move', + enabledAnchors: tool === 'move' ? ALL_ANCHORS : NO_ANCHORS, + }); +}; + +export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { + const previewLayer = getPreviewLayer(stage); + let toolPreviewGroup = previewLayer.findOne(`#${PREVIEW_TOOL_GROUP_ID}`); + if (toolPreviewGroup) { + return toolPreviewGroup; + } + + toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); // Create the brush preview group & circles const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); @@ -74,114 +231,10 @@ const getPreviewLayer = ( strokeWidth: 1, }); - const toolGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); - - toolGroup.add(rectPreview); - toolGroup.add(brushPreviewGroup); - - // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully - // transparent rect for this purpose. - const generationBboxGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP }); - const generationBboxDummyRect = new Konva.Rect({ - id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, - listening: false, - strokeEnabled: false, - draggable: true, - }); - generationBboxDummyRect.on('dragmove', (e) => { - const bbox: IRect = { - x: roundToMultiple(Math.round(generationBboxDummyRect.x()), 64), - y: roundToMultiple(Math.round(generationBboxDummyRect.y()), 64), - width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()), - height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()), - }; - generationBboxDummyRect.setAttrs(bbox); - const genBbox = $genBbox.get(); - if ( - genBbox.x !== bbox.x || - genBbox.y !== bbox.y || - genBbox.width !== bbox.width || - genBbox.height !== bbox.height - ) { - onBboxTransformed(bbox); - } - }); - const generationBboxTransformer = new Konva.Transformer({ - id: PREVIEW_GENERATION_BBOX_TRANSFORMER, - borderDash: [5, 5], - borderStroke: 'rgba(212,216,234,1)', - borderEnabled: true, - rotateEnabled: false, - keepRatio: false, - ignoreStroke: true, - listening: false, - flipEnabled: false, - anchorFill: 'rgba(212,216,234,1)', - anchorStroke: 'rgb(42,42,42)', - anchorSize: 12, - anchorCornerRadius: 3, - anchorStyleFunc: (anchor) => { - // Make the x/y resize anchors little bars - if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { - anchor.height(8); - anchor.offsetY(4); - anchor.width(30); - anchor.offsetX(15); - } - if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { - anchor.height(30); - anchor.offsetY(15); - anchor.width(8); - anchor.offsetX(4); - } - }, - }); - generationBboxTransformer.on('transform', (e) => { - const bbox: IRect = { - x: Math.round(generationBboxDummyRect.x()), - y: Math.round(generationBboxDummyRect.y()), - width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()), - height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()), - }; - generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); - onBboxTransformed(bbox); - }); - // The transformer will always be transforming the dummy rect - generationBboxTransformer.nodes([generationBboxDummyRect]); - generationBboxGroup.add(generationBboxDummyRect); - generationBboxGroup.add(generationBboxTransformer); - previewLayer.add(toolGroup); - previewLayer.add(generationBboxGroup); - - return previewLayer; -}; - -const ALL_ANCHORS: string[] = [ - 'top-left', - 'top-center', - 'top-right', - 'middle-right', - 'middle-left', - 'bottom-left', - 'bottom-center', - 'bottom-right', -]; -const NO_ANCHORS: string[] = []; - -export const renderImageDimsPreview = (stage: Konva.Stage, bbox: IRect, tool: Tool): void => { - const previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); - const generationBboxGroup = stage.findOne(`#${PREVIEW_GENERATION_BBOX_GROUP}`); - const generationBboxDummyRect = stage.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); - const generationBboxTransformer = stage.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); - assert( - previewLayer && generationBboxGroup && generationBboxDummyRect && generationBboxTransformer, - 'Generation bbox konva objects not found' - ); - generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'move' }); - generationBboxTransformer.setAttrs({ - listening: tool === 'move', - enabledAnchors: tool === 'move' ? ALL_ANCHORS : NO_ANCHORS, - }); + toolPreviewGroup.add(rectPreview); + toolPreviewGroup.add(brushPreviewGroup); + previewLayer.add(toolPreviewGroup); + return toolPreviewGroup; }; /** @@ -195,7 +248,7 @@ export const renderImageDimsPreview = (stage: Konva.Stage, bbox: IRect, tool: To * @param lastMouseDownPos The position of the last mouse down event - used for the rect tool * @param brushSize The brush size */ -export const renderPreviewLayer = ( +export const renderToolPreview = ( stage: Konva.Stage, tool: Tool, brushColor: RgbaColor, @@ -205,9 +258,7 @@ export const renderPreviewLayer = ( lastMouseDownPos: Vector2d | null, brushSize: number, isDrawing: boolean, - isMouseDown: boolean, - $genBbox: WritableAtom, - onBboxTransformed: (bbox: IRect) => void + isMouseDown: boolean ): void => { const layerCount = stage.find(selectRenderableLayers).length; // Update the stage's pointer style @@ -233,10 +284,7 @@ export const renderPreviewLayer = ( stage.draggable(tool === 'view'); - const previewLayer = getPreviewLayer(stage, $genBbox, onBboxTransformed); - const toolGroup = previewLayer.findOne(`#${PREVIEW_TOOL_GROUP_ID}`); - - assert(toolGroup, 'Tool group not found'); + const toolPreviewGroup = getToolPreviewGroup(stage); if ( !cursorPos || @@ -244,9 +292,9 @@ export const renderPreviewLayer = ( (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') ) { // We can bail early if the mouse isn't over the stage or there are no layers - toolGroup.visible(false); + toolPreviewGroup.visible(false); } else { - toolGroup.visible(true); + toolPreviewGroup.visible(true); const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); assert(brushPreviewGroup, 'Brush preview group not found'); @@ -267,11 +315,11 @@ export const renderPreviewLayer = ( }); // Update the inner border of the brush preview - const brushPreviewInner = previewLayer.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); + const brushPreviewInner = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); // Update the outer border of the brush preview - const brushPreviewOuter = previewLayer.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); + const brushPreviewOuter = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); brushPreviewOuter?.setAttrs({ x: cursorPos.x, y: cursorPos.y, @@ -285,7 +333,7 @@ export const renderPreviewLayer = ( if (cursorPos && lastMouseDownPos && tool === 'rect') { const snappedPos = snapPosToStage(cursorPos, stage); - const rectPreview = previewLayer.findOne(`#${PREVIEW_RECT_ID}`); + const rectPreview = brushPreviewGroup.findOne(`#${PREVIEW_RECT_ID}`); rectPreview?.setAttrs({ x: Math.min(snappedPos.x, lastMouseDownPos.x), y: Math.min(snappedPos.y, lastMouseDownPos.y), diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 7d590ba4cd..573b591295 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -92,8 +92,12 @@ export const initialControlLayersState: ControlLayersState = { height: 512, aspectRatio: deepClone(initialAspectRatioState), }, - x: 0, - y: 0, + bbox: { + x: 0, + y: 0, + width: 512, + height: 512, + }, }; /** @@ -800,10 +804,7 @@ export const controlLayersSlice = createSlice({ state.size.aspectRatio = action.payload; }, bboxChanged: (state, action: PayloadAction) => { - state.x = action.payload.x; - state.y = action.payload.y; - state.size.width = action.payload.width; - state.size.height = action.payload.height; + state.bbox = action.payload; }, brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = Math.round(action.payload); @@ -998,7 +999,6 @@ export const $lastAddedPoint = atom(null); export const $isSpaceDown = atom(false); export const $stageScale = atom(1); export const $stagePos = atom({ x: 0, y: 0 }); -export const $genBbox = atom({ x: 0, y: 0, width: 0, height: 0 }); // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): This is a hack, figure out another way to handle this... @@ -1007,12 +1007,13 @@ export const $brushColor = atom(DEFAULT_RGBA_COLOR); export const $brushSpacingPx = atom(0); export const $selectedLayer = atom(null); export const $shouldInvertBrushSizeScrollDirection = atom(false); +export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); export const controlLayersPersistConfig: PersistConfig = { name: controlLayersSlice.name, initialState: initialControlLayersState, migrate: migrateControlLayersState, - persistDenylist: [], + persistDenylist: ['bbox'], }; // These actions are _individually_ grouped together as single undoable actions diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index d229eb1e45..04a18b3839 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -269,8 +269,7 @@ export type ControlLayersState = { height: ParameterHeight; aspectRatio: AspectRatioState; }; - x: number; - y: number; + bbox: IRect; }; export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] };