feat(ui): wip generation bbox

This commit is contained in:
psychedelicious 2024-06-12 09:13:25 +10:00
parent 5ab345ee63
commit 9047f6db30
8 changed files with 273 additions and 192 deletions

View File

@ -8,3 +8,7 @@ export const roundDownToMultipleMin = (num: number, multiple: number): number =>
export const roundToMultiple = (num: number, multiple: number): number => { export const roundToMultiple = (num: number, multiple: number): number => {
return Math.round(num / multiple) * multiple; return Math.round(num / multiple) * multiple;
}; };
export const roundToMultipleMin = (num: number, multiple: number): number => {
return Math.max(Math.round(num / multiple) * multiple, multiple);
};

View File

@ -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 (
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>
<HUDItem label="Scale" value={stageScale} />
<HUDItem label="Layer Count" value={layerCount} />
<HUDItem label="BBox Size" value={`${bbox.width}×${bbox.height}`} />
<HUDItem label="BBox Position" value={`${bbox.x}, ${bbox.y}`} />
<HUDItem label="BBox Width % 8" value={round(bbox.width % 8, 3)} />
<HUDItem label="BBox Height % 8" value={round(bbox.height % 8, 3)} />
<HUDItem label="BBox X % 8" value={round(bbox.x % 8, 3)} />
<HUDItem label="BBox Y % 8" value={round(bbox.y % 8, 3)} />
</Flex>
);
});
HeadsUpDisplay.displayName = 'HeadsUpDisplay';
const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => {
return (
<Box display="inline-block" lineHeight={1}>
<Text as="span">{label}: </Text>
<Text as="span" fontWeight="semibold">
{value}
</Text>
</Box>
);
});
HUDItem.displayName = 'HUDItem';

View File

@ -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 { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
import { import {
BRUSH_SPACING_PCT, BRUSH_SPACING_PCT,
MAX_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX,
@ -12,12 +13,11 @@ import {
} from 'features/controlLayers/konva/constants'; } from 'features/controlLayers/konva/constants';
import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { setStageEventHandlers } from 'features/controlLayers/konva/events';
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/previewLayer';
import { import {
$bbox,
$brushColor, $brushColor,
$brushSize, $brushSize,
$brushSpacingPx, $brushSpacingPx,
$genBbox,
$isDrawing, $isDrawing,
$isMouseDown, $isMouseDown,
$isSpaceDown, $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), () => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX),
[state.brushSize] [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(() => { useLayoutEffect(() => {
$brushColor.set(brushColor); $brushColor.set(brushColor);
@ -111,7 +107,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
$brushSpacingPx.set(brushSpacingPx); $brushSpacingPx.set(brushSpacingPx);
$selectedLayer.set(selectedLayer); $selectedLayer.set(selectedLayer);
$shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection);
$genBbox.set(bbox); $bbox.set(state.bbox);
}, [ }, [
brushSpacingPx, brushSpacingPx,
brushColor, brushColor,
@ -120,7 +116,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
state.brushSize, state.brushSize,
state.selectedLayerId, state.selectedLayerId,
state.brushColor, state.brushColor,
bbox, state.bbox,
]); ]);
const onLayerPosChanged = useCallback( const onLayerPosChanged = useCallback(
@ -257,7 +253,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
return; return;
} }
log.trace('Rendering tool preview'); log.trace('Rendering tool preview');
renderers.renderPreviewLayer( renderers.renderToolPreview(
stage, stage,
tool, tool,
brushColor, brushColor,
@ -267,41 +263,36 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
lastMouseDownPos, lastMouseDownPos,
state.brushSize, state.brushSize,
isDrawing, isDrawing,
isMouseDown, isMouseDown
$genBbox,
onBboxTransformed
); );
renderImageDimsPreview(stage, bbox, tool);
}, [ }, [
asPreview, asPreview,
stage,
tool,
brushColor, brushColor,
selectedLayer,
state.globalMaskLayerOpacity,
lastCursorPos,
lastMouseDownPos,
state.brushSize,
renderers,
isDrawing, isDrawing,
isMouseDown, isMouseDown,
bbox, lastCursorPos,
stageScale, lastMouseDownPos,
onBboxTransformed, 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(() => { useLayoutEffect(() => {
log.trace('Rendering layers'); log.trace('Rendering layers');
renderers.renderLayers( renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged);
stage, }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]);
bbox,
state.layers,
state.globalMaskLayerOpacity,
tool,
getImageDTO,
onLayerPosChanged
);
}, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers, bbox]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (asPreview) { if (asPreview) {
@ -369,6 +360,11 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
overflow="hidden" overflow="hidden"
data-testid="control-layers-canvas" data-testid="control-layers-canvas"
/> />
{!asPreview && (
<Flex position="absolute" top={0} insetInlineStart={0}>
<HeadsUpDisplay />
</Flex>
)}
</Flex> </Flex>
); );
}); });

View File

@ -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 { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming';
import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; import type { ControlAdapterLayer } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
/** /**
@ -52,7 +51,6 @@ const updateCALayerImageSource = async (
stage: Konva.Stage, stage: Konva.Stage,
konvaLayer: Konva.Layer, konvaLayer: Konva.Layer,
layerState: ControlAdapterLayer, layerState: ControlAdapterLayer,
bbox: IRect,
getImageDTO: (imageName: string) => Promise<ImageDTO | null> getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): Promise<void> => { ): Promise<void> => {
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
@ -74,7 +72,7 @@ const updateCALayerImageSource = async (
id: imageId, id: imageId,
image: imageEl, image: imageEl,
}); });
updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); updateCALayerImageAttrs(stage, konvaImage, layerState);
// Must cache after this to apply the filters // Must cache after this to apply the filters
konvaImage.cache(); konvaImage.cache();
imageEl.id = imageId; imageEl.id = imageId;
@ -95,8 +93,7 @@ const updateCALayerImageSource = async (
const updateCALayerImageAttrs = ( const updateCALayerImageAttrs = (
stage: Konva.Stage, stage: Konva.Stage,
konvaImage: Konva.Image, konvaImage: Konva.Image,
layerState: ControlAdapterLayer, layerState: ControlAdapterLayer
bbox: IRect
): void => { ): void => {
let needsCache = false; let needsCache = false;
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, // 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. // TODO(psyche): Investigate and report upstream.
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
if ( if (
konvaImage.x() !== bbox.x || konvaImage.x() !== layerState.x ||
konvaImage.y() !== bbox.y || konvaImage.y() !== layerState.y ||
konvaImage.width() !== bbox.width ||
konvaImage.height() !== bbox.height ||
konvaImage.visible() !== layerState.isEnabled || konvaImage.visible() !== layerState.isEnabled ||
hasFilter !== layerState.isFilterEnabled hasFilter !== layerState.isFilterEnabled
) { ) {
@ -115,7 +110,6 @@ const updateCALayerImageAttrs = (
opacity: layerState.opacity, opacity: layerState.opacity,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
...bbox,
visible: layerState.isEnabled, visible: layerState.isEnabled,
filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [],
}); });
@ -139,7 +133,6 @@ const updateCALayerImageAttrs = (
export const renderCALayer = ( export const renderCALayer = (
stage: Konva.Stage, stage: Konva.Stage,
layerState: ControlAdapterLayer, layerState: ControlAdapterLayer,
bbox: IRect,
zIndex: number, zIndex: number,
getImageDTO: (imageName: string) => Promise<ImageDTO | null> getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): void => { ): void => {
@ -164,8 +157,8 @@ export const renderCALayer = (
} }
if (imageSourceNeedsUpdate) { if (imageSourceNeedsUpdate) {
updateCALayerImageSource(stage, konvaLayer, layerState, bbox, getImageDTO); updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO);
} else if (konvaImage) { } else if (konvaImage) {
updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); updateCALayerImageAttrs(stage, konvaImage, layerState);
} }
}; };

View File

@ -3,7 +3,7 @@ import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming';
import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer';
import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; 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 { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util';
@ -16,7 +16,6 @@ import {
isRenderableLayer, isRenderableLayer,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
import type Konva from 'konva'; import type Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
@ -35,7 +34,6 @@ import type { ImageDTO } from 'services/api/types';
*/ */
const renderLayers = ( const renderLayers = (
stage: Konva.Stage, stage: Konva.Stage,
bbox: IRect,
layerStates: Layer[], layerStates: Layer[],
globalMaskLayerOpacity: number, globalMaskLayerOpacity: number,
tool: Tool, tool: Tool,
@ -56,7 +54,7 @@ const renderLayers = (
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged);
} }
if (isControlAdapterLayer(layer)) { if (isControlAdapterLayer(layer)) {
renderCALayer(stage, layer, bbox, zIndex, getImageDTO); renderCALayer(stage, layer, zIndex, getImageDTO);
} }
if (isInitialImageLayer(layer)) { if (isInitialImageLayer(layer)) {
renderIILayer(stage, layer, zIndex, getImageDTO); renderIILayer(stage, layer, zIndex, getImageDTO);
@ -76,7 +74,8 @@ const renderLayers = (
* All the renderers for the Konva stage. * All the renderers for the Konva stage.
*/ */
export const renderers = { export const renderers = {
renderPreviewLayer, renderToolPreview,
renderBboxPreview,
renderLayers, renderLayers,
updateBboxes, updateBboxes,
}; };
@ -87,7 +86,8 @@ export const renderers = {
* @returns The renderers with debouncing applied * @returns The renderers with debouncing applied
*/ */
const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({
renderPreviewLayer: debounce(renderPreviewLayer, ms), renderToolPreview: debounce(renderToolPreview, ms),
renderBboxPreview: debounce(renderBboxPreview, ms),
renderLayers: debounce(renderLayers, ms), renderLayers: debounce(renderLayers, ms),
updateBboxes: debounce(updateBboxes, ms), updateBboxes: debounce(updateBboxes, ms),
}); });

View File

@ -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 { rgbaColorToString } from 'features/canvas/util/colorToString';
import { import {
BBOX_SELECTED_STROKE, 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 type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import type { WritableAtom } from 'nanostores';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
/** /**
* Creates the singleton preview layer and all its objects. * Creates the singleton preview layer and all its objects.
* @param stage The konva stage * @param stage The konva stage
*/ */
const getPreviewLayer = ( const getPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
stage: Konva.Stage,
$genBbox: WritableAtom<IRect>,
onBboxTransformed: (bbox: IRect) => void
): Konva.Layer => {
let previewLayer = stage.findOne<Konva.Layer>(`#${PREVIEW_LAYER_ID}`); let previewLayer = stage.findOne<Konva.Layer>(`#${PREVIEW_LAYER_ID}`);
if (previewLayer) { if (previewLayer) {
return previewLayer; return previewLayer;
@ -40,6 +35,168 @@ const getPreviewLayer = (
// Initialize the preview layer & add to the stage // Initialize the preview layer & add to the stage
previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true });
stage.add(previewLayer); 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<Konva.Group>(`#${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<Konva.Rect>(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`);
const bboxTransformer = bboxGroup.findOne<Konva.Transformer>(`#${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<Konva.Group>(`#${PREVIEW_TOOL_GROUP_ID}`);
if (toolPreviewGroup) {
return toolPreviewGroup;
}
toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID });
// Create the brush preview group & circles // Create the brush preview group & circles
const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID });
@ -74,114 +231,10 @@ const getPreviewLayer = (
strokeWidth: 1, strokeWidth: 1,
}); });
const toolGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); toolPreviewGroup.add(rectPreview);
toolPreviewGroup.add(brushPreviewGroup);
toolGroup.add(rectPreview); previewLayer.add(toolPreviewGroup);
toolGroup.add(brushPreviewGroup); return toolPreviewGroup;
// 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<Konva.Layer>(`#${PREVIEW_LAYER_ID}`);
const generationBboxGroup = stage.findOne<Konva.Rect>(`#${PREVIEW_GENERATION_BBOX_GROUP}`);
const generationBboxDummyRect = stage.findOne<Konva.Rect>(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`);
const generationBboxTransformer = stage.findOne<Konva.Transformer>(`#${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,
});
}; };
/** /**
@ -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 lastMouseDownPos The position of the last mouse down event - used for the rect tool
* @param brushSize The brush size * @param brushSize The brush size
*/ */
export const renderPreviewLayer = ( export const renderToolPreview = (
stage: Konva.Stage, stage: Konva.Stage,
tool: Tool, tool: Tool,
brushColor: RgbaColor, brushColor: RgbaColor,
@ -205,9 +258,7 @@ export const renderPreviewLayer = (
lastMouseDownPos: Vector2d | null, lastMouseDownPos: Vector2d | null,
brushSize: number, brushSize: number,
isDrawing: boolean, isDrawing: boolean,
isMouseDown: boolean, isMouseDown: boolean
$genBbox: WritableAtom<IRect>,
onBboxTransformed: (bbox: IRect) => void
): void => { ): void => {
const layerCount = stage.find(selectRenderableLayers).length; const layerCount = stage.find(selectRenderableLayers).length;
// Update the stage's pointer style // Update the stage's pointer style
@ -233,10 +284,7 @@ export const renderPreviewLayer = (
stage.draggable(tool === 'view'); stage.draggable(tool === 'view');
const previewLayer = getPreviewLayer(stage, $genBbox, onBboxTransformed); const toolPreviewGroup = getToolPreviewGroup(stage);
const toolGroup = previewLayer.findOne<Konva.Group>(`#${PREVIEW_TOOL_GROUP_ID}`);
assert(toolGroup, 'Tool group not found');
if ( if (
!cursorPos || !cursorPos ||
@ -244,9 +292,9 @@ export const renderPreviewLayer = (
(selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer')
) { ) {
// We can bail early if the mouse isn't over the stage or there are no layers // We can bail early if the mouse isn't over the stage or there are no layers
toolGroup.visible(false); toolPreviewGroup.visible(false);
} else { } else {
toolGroup.visible(true); toolPreviewGroup.visible(true);
const brushPreviewGroup = stage.findOne<Konva.Group>(`#${PREVIEW_BRUSH_GROUP_ID}`); const brushPreviewGroup = stage.findOne<Konva.Group>(`#${PREVIEW_BRUSH_GROUP_ID}`);
assert(brushPreviewGroup, 'Brush preview group not found'); assert(brushPreviewGroup, 'Brush preview group not found');
@ -267,11 +315,11 @@ export const renderPreviewLayer = (
}); });
// Update the inner border of the brush preview // Update the inner border of the brush preview
const brushPreviewInner = previewLayer.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); const brushPreviewInner = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`);
brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
// Update the outer border of the brush preview // Update the outer border of the brush preview
const brushPreviewOuter = previewLayer.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); const brushPreviewOuter = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`);
brushPreviewOuter?.setAttrs({ brushPreviewOuter?.setAttrs({
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y, y: cursorPos.y,
@ -285,7 +333,7 @@ export const renderPreviewLayer = (
if (cursorPos && lastMouseDownPos && tool === 'rect') { if (cursorPos && lastMouseDownPos && tool === 'rect') {
const snappedPos = snapPosToStage(cursorPos, stage); const snappedPos = snapPosToStage(cursorPos, stage);
const rectPreview = previewLayer.findOne<Konva.Rect>(`#${PREVIEW_RECT_ID}`); const rectPreview = brushPreviewGroup.findOne<Konva.Rect>(`#${PREVIEW_RECT_ID}`);
rectPreview?.setAttrs({ rectPreview?.setAttrs({
x: Math.min(snappedPos.x, lastMouseDownPos.x), x: Math.min(snappedPos.x, lastMouseDownPos.x),
y: Math.min(snappedPos.y, lastMouseDownPos.y), y: Math.min(snappedPos.y, lastMouseDownPos.y),

View File

@ -92,8 +92,12 @@ export const initialControlLayersState: ControlLayersState = {
height: 512, height: 512,
aspectRatio: deepClone(initialAspectRatioState), aspectRatio: deepClone(initialAspectRatioState),
}, },
x: 0, bbox: {
y: 0, x: 0,
y: 0,
width: 512,
height: 512,
},
}; };
/** /**
@ -800,10 +804,7 @@ export const controlLayersSlice = createSlice({
state.size.aspectRatio = action.payload; state.size.aspectRatio = action.payload;
}, },
bboxChanged: (state, action: PayloadAction<IRect>) => { bboxChanged: (state, action: PayloadAction<IRect>) => {
state.x = action.payload.x; state.bbox = action.payload;
state.y = action.payload.y;
state.size.width = action.payload.width;
state.size.height = action.payload.height;
}, },
brushSizeChanged: (state, action: PayloadAction<number>) => { brushSizeChanged: (state, action: PayloadAction<number>) => {
state.brushSize = Math.round(action.payload); state.brushSize = Math.round(action.payload);
@ -998,7 +999,6 @@ export const $lastAddedPoint = atom<Vector2d | null>(null);
export const $isSpaceDown = atom(false); export const $isSpaceDown = atom(false);
export const $stageScale = atom<number>(1); export const $stageScale = atom<number>(1);
export const $stagePos = atom<Vector2d>({ x: 0, y: 0 }); export const $stagePos = atom<Vector2d>({ x: 0, y: 0 });
export const $genBbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 });
// Some nanostores that are manually synced to redux state to provide imperative access // 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... // TODO(psyche): This is a hack, figure out another way to handle this...
@ -1007,12 +1007,13 @@ export const $brushColor = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
export const $brushSpacingPx = atom<number>(0); export const $brushSpacingPx = atom<number>(0);
export const $selectedLayer = atom<Layer | null>(null); export const $selectedLayer = atom<Layer | null>(null);
export const $shouldInvertBrushSizeScrollDirection = atom(false); export const $shouldInvertBrushSizeScrollDirection = atom(false);
export const $bbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 });
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = { export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
name: controlLayersSlice.name, name: controlLayersSlice.name,
initialState: initialControlLayersState, initialState: initialControlLayersState,
migrate: migrateControlLayersState, migrate: migrateControlLayersState,
persistDenylist: [], persistDenylist: ['bbox'],
}; };
// These actions are _individually_ grouped together as single undoable actions // These actions are _individually_ grouped together as single undoable actions

View File

@ -269,8 +269,7 @@ export type ControlLayersState = {
height: ParameterHeight; height: ParameterHeight;
aspectRatio: AspectRatioState; aspectRatio: AspectRatioState;
}; };
x: number; bbox: IRect;
y: number;
}; };
export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] };