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] };