mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip generation bbox
This commit is contained in:
parent
5ab345ee63
commit
9047f6db30
@ -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);
|
||||
};
|
||||
|
@ -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';
|
@ -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 && (
|
||||
<Flex position="absolute" top={0} insetInlineStart={0}>
|
||||
<HeadsUpDisplay />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
@ -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<ImageDTO | null>
|
||||
): Promise<void> => {
|
||||
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<ImageDTO | null>
|
||||
): 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);
|
||||
}
|
||||
};
|
||||
|
@ -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),
|
||||
});
|
||||
|
@ -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<IRect>,
|
||||
onBboxTransformed: (bbox: IRect) => void
|
||||
): Konva.Layer => {
|
||||
const getPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
|
||||
let previewLayer = stage.findOne<Konva.Layer>(`#${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<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
|
||||
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<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,
|
||||
});
|
||||
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<IRect>,
|
||||
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<Konva.Group>(`#${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<Konva.Group>(`#${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<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 });
|
||||
|
||||
// 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({
|
||||
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<Konva.Rect>(`#${PREVIEW_RECT_ID}`);
|
||||
const rectPreview = brushPreviewGroup.findOne<Konva.Rect>(`#${PREVIEW_RECT_ID}`);
|
||||
rectPreview?.setAttrs({
|
||||
x: Math.min(snappedPos.x, lastMouseDownPos.x),
|
||||
y: Math.min(snappedPos.y, lastMouseDownPos.y),
|
||||
|
@ -92,8 +92,12 @@ export const initialControlLayersState: ControlLayersState = {
|
||||
height: 512,
|
||||
aspectRatio: deepClone(initialAspectRatioState),
|
||||
},
|
||||
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<IRect>) => {
|
||||
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<number>) => {
|
||||
state.brushSize = Math.round(action.payload);
|
||||
@ -998,7 +999,6 @@ export const $lastAddedPoint = atom<Vector2d | null>(null);
|
||||
export const $isSpaceDown = atom(false);
|
||||
export const $stageScale = atom<number>(1);
|
||||
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
|
||||
// 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 $selectedLayer = atom<Layer | null>(null);
|
||||
export const $shouldInvertBrushSizeScrollDirection = atom(false);
|
||||
export const $bbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 });
|
||||
|
||||
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
|
||||
name: controlLayersSlice.name,
|
||||
initialState: initialControlLayersState,
|
||||
migrate: migrateControlLayersState,
|
||||
persistDenylist: [],
|
||||
persistDenylist: ['bbox'],
|
||||
};
|
||||
|
||||
// These actions are _individually_ grouped together as single undoable actions
|
||||
|
@ -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] };
|
||||
|
Loading…
Reference in New Issue
Block a user