mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
329 lines
9.6 KiB
TypeScript
329 lines
9.6 KiB
TypeScript
import { createSelector } from '@reduxjs/toolkit';
|
|
import Konva from 'konva';
|
|
import { KonvaEventObject } from 'konva/lib/Node';
|
|
import { Vector2d } from 'konva/lib/types';
|
|
import _ from 'lodash';
|
|
import { useEffect, useRef } from 'react';
|
|
import { Group, Rect, Transformer } from 'react-konva';
|
|
import {
|
|
RootState,
|
|
useAppDispatch,
|
|
useAppSelector,
|
|
} from '../../../../app/store';
|
|
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
|
import {
|
|
InpaintingState,
|
|
setBoundingBoxCoordinate,
|
|
setBoundingBoxDimensions,
|
|
setIsBoundingBoxTransforming,
|
|
setIsDrawing,
|
|
setShouldShowBrush,
|
|
} from '../inpaintingSlice';
|
|
import { rgbaColorToString } from '../util/colorToString';
|
|
import {
|
|
DASH_WIDTH,
|
|
// MARCHING_ANTS_SPEED,
|
|
} from '../util/constants';
|
|
|
|
const boundingBoxPreviewSelector = createSelector(
|
|
(state: RootState) => state.inpainting,
|
|
(inpainting: InpaintingState) => {
|
|
const {
|
|
boundingBoxCoordinate,
|
|
boundingBoxDimensions,
|
|
boundingBoxPreviewFill,
|
|
canvasDimensions,
|
|
stageScale,
|
|
imageToInpaint,
|
|
isMovingBoundingBox,
|
|
} = inpainting;
|
|
return {
|
|
boundingBoxCoordinate,
|
|
boundingBoxDimensions,
|
|
boundingBoxPreviewFillString: rgbaColorToString(boundingBoxPreviewFill),
|
|
canvasDimensions,
|
|
stageScale,
|
|
imageToInpaint,
|
|
dash: DASH_WIDTH / stageScale, // scale dash lengths
|
|
strokeWidth: 1 / stageScale, // scale stroke thickness
|
|
isMovingBoundingBox,
|
|
};
|
|
},
|
|
{
|
|
memoizeOptions: {
|
|
resultEqualityCheck: _.isEqual,
|
|
},
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Shades the area around the mask.
|
|
*/
|
|
export const InpaintingBoundingBoxPreviewOverlay = () => {
|
|
const {
|
|
boundingBoxCoordinate,
|
|
boundingBoxDimensions,
|
|
boundingBoxPreviewFillString,
|
|
canvasDimensions,
|
|
} = useAppSelector(boundingBoxPreviewSelector);
|
|
|
|
return (
|
|
<Group>
|
|
<Rect
|
|
x={0}
|
|
y={0}
|
|
height={canvasDimensions.height}
|
|
width={canvasDimensions.width}
|
|
fill={boundingBoxPreviewFillString}
|
|
/>
|
|
<Rect
|
|
x={boundingBoxCoordinate.x}
|
|
y={boundingBoxCoordinate.y}
|
|
width={boundingBoxDimensions.width}
|
|
height={boundingBoxDimensions.height}
|
|
fill={'rgb(255,255,255)'}
|
|
listening={false}
|
|
globalCompositeOperation={'destination-out'}
|
|
/>
|
|
</Group>
|
|
);
|
|
};
|
|
|
|
// /**
|
|
// * Draws marching ants around the mask. Unused.
|
|
// */
|
|
// const _InpaintingBoundingBoxPreviewMarchingAnts = () => {
|
|
// const { boundingBoxCoordinate, boundingBoxDimensions } = useAppSelector(
|
|
// boundingBoxPreviewSelector
|
|
// );
|
|
|
|
// const blackStrokeRectRef = useRef<Konva.Rect>(null);
|
|
// const whiteStrokeRectRef = useRef<Konva.Rect>(null);
|
|
|
|
// useEffect(() => {
|
|
// const blackStrokeRect = blackStrokeRectRef.current;
|
|
// const whiteStrokeRect = whiteStrokeRectRef.current;
|
|
|
|
// const anim = new Konva.Animation((frame) => {
|
|
// if (!frame) return;
|
|
// blackStrokeRect?.dashOffset(
|
|
// -1 * (Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16)
|
|
// );
|
|
// whiteStrokeRect?.dashOffset(
|
|
// -1 * ((Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16) + 4)
|
|
// );
|
|
// });
|
|
|
|
// anim.start();
|
|
|
|
// return () => {
|
|
// anim.stop();
|
|
// };
|
|
// }, []);
|
|
|
|
// return (
|
|
// <Group>
|
|
// <Rect
|
|
// x={boundingBoxCoordinate.x}
|
|
// y={boundingBoxCoordinate.y}
|
|
// width={boundingBoxDimensions.width}
|
|
// height={boundingBoxDimensions.height}
|
|
// stroke={'black'}
|
|
// strokeWidth={1}
|
|
// dash={[4, 4]}
|
|
// ref={blackStrokeRectRef}
|
|
// listening={false}
|
|
// />
|
|
// <Rect
|
|
// x={boundingBoxCoordinate.x}
|
|
// y={boundingBoxCoordinate.y}
|
|
// width={boundingBoxDimensions.width}
|
|
// height={boundingBoxDimensions.height}
|
|
// stroke={'white'}
|
|
// dash={[4, 4]}
|
|
// strokeWidth={1}
|
|
// ref={whiteStrokeRectRef}
|
|
// listening={false}
|
|
// />
|
|
// </Group>
|
|
// );
|
|
// };
|
|
|
|
const InpaintingBoundingBoxPreview = () => {
|
|
const dispatch = useAppDispatch();
|
|
const {
|
|
boundingBoxCoordinate,
|
|
boundingBoxDimensions,
|
|
strokeWidth,
|
|
stageScale,
|
|
imageToInpaint,
|
|
} = useAppSelector(boundingBoxPreviewSelector);
|
|
|
|
const transformerRef = useRef<Konva.Transformer>(null);
|
|
const shapeRef = useRef<Konva.Rect>(null);
|
|
|
|
useEffect(() => {
|
|
if (!transformerRef.current || !shapeRef.current) return;
|
|
transformerRef.current.nodes([shapeRef.current]);
|
|
transformerRef.current.getLayer()?.batchDraw();
|
|
}, []);
|
|
|
|
const scaledStep = 64 * stageScale;
|
|
|
|
return (
|
|
<>
|
|
<Rect
|
|
x={boundingBoxCoordinate.x}
|
|
y={boundingBoxCoordinate.y}
|
|
width={boundingBoxDimensions.width}
|
|
height={boundingBoxDimensions.height}
|
|
ref={shapeRef}
|
|
stroke={'white'}
|
|
strokeWidth={strokeWidth}
|
|
listening={false}
|
|
onTransformStart={() => {
|
|
dispatch(setIsDrawing(false));
|
|
dispatch(setShouldShowBrush(false));
|
|
dispatch(setIsBoundingBoxTransforming(true));
|
|
}}
|
|
onTransformEnd={() => {
|
|
dispatch(setShouldShowBrush(true));
|
|
dispatch(setIsBoundingBoxTransforming(false));
|
|
}}
|
|
onTransform={() => {
|
|
/**
|
|
* The Konva Transformer changes the object's anchor point and scale factor,
|
|
* not its width and height. We need to un-scale the width and height before
|
|
* setting the values.
|
|
*/
|
|
if (!shapeRef.current) return;
|
|
|
|
const rect = shapeRef.current;
|
|
|
|
const scaleX = rect.scaleX();
|
|
const scaleY = rect.scaleY();
|
|
|
|
// undo the scaling
|
|
const width = Math.round(rect.width() * scaleX);
|
|
const height = Math.round(rect.height() * scaleY);
|
|
|
|
const x = Math.round(rect.x());
|
|
const y = Math.round(rect.y());
|
|
|
|
dispatch(
|
|
setBoundingBoxDimensions({
|
|
width,
|
|
height,
|
|
})
|
|
);
|
|
|
|
dispatch(
|
|
setBoundingBoxCoordinate({
|
|
x,
|
|
y,
|
|
})
|
|
);
|
|
|
|
// Reset the scale now that the coords/dimensions have been un-scaled
|
|
rect.scaleX(1);
|
|
rect.scaleY(1);
|
|
}}
|
|
/>
|
|
<Transformer
|
|
ref={transformerRef}
|
|
rotateEnabled={false}
|
|
anchorSize={15}
|
|
anchorFill={'rgba(212,216,234,1)'}
|
|
anchorStroke={'rgb(42,42,42)'}
|
|
borderEnabled={true}
|
|
borderStroke={'black'}
|
|
borderDash={[4, 4]}
|
|
anchorCornerRadius={3}
|
|
ignoreStroke={true}
|
|
keepRatio={false}
|
|
flipEnabled={false}
|
|
onMouseDown={(e: KonvaEventObject<MouseEvent>) => {
|
|
e.cancelBubble = true;
|
|
}}
|
|
anchorDragBoundFunc={(
|
|
oldPos: Vector2d, // old absolute position of anchor point
|
|
newPos: Vector2d, // new absolute position (potentially) of anchor point
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
_e: MouseEvent
|
|
) => {
|
|
/**
|
|
* Konva does not transform with width or height. It transforms the anchor point
|
|
* and scale factor. This is then sent to the shape's onTransform listeners.
|
|
*
|
|
* We need to snap the new width to steps of 64 without also snapping the
|
|
* coordinates of the bounding box to steps of 64. But because the whole
|
|
* stage is scaled, our actual desired step is actually 64 * the stage scale.
|
|
*/
|
|
|
|
// Difference of the old coords from the nearest multiple the scaled step
|
|
const offsetX = oldPos.x % scaledStep;
|
|
const offsetY = oldPos.y % scaledStep;
|
|
|
|
// Round new position to the nearest multiple of the scaled step
|
|
const closestX = roundToMultiple(newPos.x, scaledStep) + offsetX;
|
|
const closestY = roundToMultiple(newPos.y, scaledStep) + offsetY;
|
|
|
|
// the difference between the old coord and new
|
|
const diffX = Math.abs(newPos.x - closestX);
|
|
const diffY = Math.abs(newPos.y - closestY);
|
|
|
|
// if the difference is less than the scaled step, we want to snap
|
|
const didSnapX = diffX < scaledStep;
|
|
const didSnapY = diffY < scaledStep;
|
|
|
|
// We may not change anything, stash the old position
|
|
let newCoordinate = { ...oldPos };
|
|
|
|
// Set the new coords based on what snapped
|
|
if (didSnapX && !didSnapY) {
|
|
newCoordinate = {
|
|
x: closestX,
|
|
y: oldPos.y,
|
|
};
|
|
} else if (!didSnapX && didSnapY) {
|
|
newCoordinate = {
|
|
x: oldPos.x,
|
|
y: closestY,
|
|
};
|
|
} else if (didSnapX && didSnapY) {
|
|
newCoordinate = {
|
|
x: closestX,
|
|
y: closestY,
|
|
};
|
|
}
|
|
|
|
return newCoordinate;
|
|
}}
|
|
boundBoxFunc={(oldBoundBox, newBoundBox) => {
|
|
/**
|
|
* The transformer uses this callback to limit valid transformations.
|
|
* Unlike anchorDragBoundFunc, it does get a width and height, so
|
|
* the logic to constrain the size of the bounding box is very simple.
|
|
*/
|
|
if (!imageToInpaint) return oldBoundBox;
|
|
|
|
if (
|
|
newBoundBox.width + newBoundBox.x >
|
|
imageToInpaint.width * stageScale ||
|
|
newBoundBox.height + newBoundBox.y >
|
|
imageToInpaint.height * stageScale ||
|
|
newBoundBox.x < 0 ||
|
|
newBoundBox.y < 0
|
|
) {
|
|
return oldBoundBox;
|
|
}
|
|
|
|
return newBoundBox;
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default InpaintingBoundingBoxPreview;
|