Improves bounding box behavior

This commit is contained in:
psychedelicious 2022-10-28 13:59:52 +11:00
parent bbe53841e4
commit 27ba91e74d
6 changed files with 268 additions and 234 deletions

View File

@ -133,7 +133,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
{ {
title: 'Quick Toggle Brush/Eraser', title: 'Quick Toggle Brush/Eraser',
desc: 'Quick toggle between brush and eraser', desc: 'Quick toggle between brush and eraser',
hotkey: 'Alt', hotkey: 'Z',
}, },
{ {
title: 'Decrease Brush Size', title: 'Decrease Brush Size',

View File

@ -16,10 +16,8 @@ import { useAppDispatch, useAppSelector } from '../../../app/store';
import { import {
addLine, addLine,
addPointToCurrentLine, addPointToCurrentLine,
setBoundingBoxCoordinate,
setCursorPosition, setCursorPosition,
setIsDrawing, setIsDrawing,
setIsMovingBoundingBox,
} from './inpaintingSlice'; } from './inpaintingSlice';
import { inpaintingCanvasSelector } from './inpaintingSliceSelectors'; import { inpaintingCanvasSelector } from './inpaintingSliceSelectors';
@ -30,7 +28,6 @@ import InpaintingCanvasBrushPreviewOutline from './components/InpaintingCanvasBr
import Cacher from './components/Cacher'; import Cacher from './components/Cacher';
import { Vector2d } from 'konva/lib/types'; import { Vector2d } from 'konva/lib/types';
import getScaledCursorPosition from './util/getScaledCursorPosition'; import getScaledCursorPosition from './util/getScaledCursorPosition';
import _ from 'lodash';
import InpaintingBoundingBoxPreview, { import InpaintingBoundingBoxPreview, {
InpaintingBoundingBoxPreviewOverlay, InpaintingBoundingBoxPreviewOverlay,
} from './components/InpaintingBoundingBoxPreview'; } from './components/InpaintingBoundingBoxPreview';
@ -53,14 +50,10 @@ const InpaintingCanvas = () => {
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
maskColor, maskColor,
imageToInpaint, imageToInpaint,
isMovingBoundingBox,
boundingBoxDimensions,
canvasDimensions,
boundingBoxCoordinate,
stageScale, stageScale,
shouldShowBoundingBoxFill, shouldShowBoundingBoxFill,
isDrawing, isDrawing,
isTransformingBoundingBox, shouldLockBoundingBox,
shouldShowBoundingBox, shouldShowBoundingBox,
} = useAppSelector(inpaintingCanvasSelector); } = useAppSelector(inpaintingCanvasSelector);
@ -91,12 +84,22 @@ const InpaintingCanvas = () => {
} }
}, [imageToInpaint, dispatch, stageScale]); }, [imageToInpaint, dispatch, stageScale]);
/**
*
* Canvas onMouseDown
*
*/
const handleMouseDown = useCallback(() => { const handleMouseDown = useCallback(() => {
if (!stageRef.current) return; if (!stageRef.current) return;
const scaledCursorPosition = getScaledCursorPosition(stageRef.current); const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
if (!scaledCursorPosition || !maskLayerRef.current) return; if (
!scaledCursorPosition ||
!maskLayerRef.current ||
!shouldLockBoundingBox
)
return;
dispatch(setIsDrawing(true)); dispatch(setIsDrawing(true));
@ -108,9 +111,15 @@ const InpaintingCanvas = () => {
points: [scaledCursorPosition.x, scaledCursorPosition.y], points: [scaledCursorPosition.x, scaledCursorPosition.y],
}) })
); );
}, [dispatch, brushSize, tool]); }, [dispatch, brushSize, tool, shouldLockBoundingBox]);
const handleMouseMove = useCallback(() => { /**
*
* Canvas onMouseMove
*
*/
const handleMouseMove = useCallback(
() => {
if (!stageRef.current) return; if (!stageRef.current) return;
const scaledCursorPosition = getScaledCursorPosition(stageRef.current); const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
@ -119,37 +128,12 @@ const InpaintingCanvas = () => {
dispatch(setCursorPosition(scaledCursorPosition)); dispatch(setCursorPosition(scaledCursorPosition));
if (!maskLayerRef.current) { if (!maskLayerRef.current || !shouldLockBoundingBox) {
return; return;
} }
const deltaX = lastCursorPosition.current.x - scaledCursorPosition.x;
const deltaY = lastCursorPosition.current.y - scaledCursorPosition.y;
lastCursorPosition.current = scaledCursorPosition; lastCursorPosition.current = scaledCursorPosition;
if (isMovingBoundingBox) {
const x = _.clamp(
Math.floor(boundingBoxCoordinate.x - deltaX),
0,
canvasDimensions.width - boundingBoxDimensions.width
);
const y = _.clamp(
Math.floor(boundingBoxCoordinate.y - deltaY),
0,
canvasDimensions.height - boundingBoxDimensions.height
);
dispatch(
setBoundingBoxCoordinate({
x,
y,
})
);
return;
}
if (!isDrawing) return; if (!isDrawing) return;
didMouseMoveRef.current = true; didMouseMoveRef.current = true;
@ -157,20 +141,25 @@ const InpaintingCanvas = () => {
dispatch( dispatch(
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y]) addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
); );
}, [ },
dispatch, [dispatch, isDrawing, shouldLockBoundingBox]
isMovingBoundingBox, );
boundingBoxDimensions,
canvasDimensions,
boundingBoxCoordinate,
isDrawing,
]);
/**
*
* Canvas onMouseUp
*
*/
const handleMouseUp = useCallback(() => { const handleMouseUp = useCallback(() => {
if (!didMouseMoveRef.current && isDrawing && stageRef.current) { if (!didMouseMoveRef.current && isDrawing && stageRef.current) {
const scaledCursorPosition = getScaledCursorPosition(stageRef.current); const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
if (!scaledCursorPosition || !maskLayerRef.current) return; if (
!scaledCursorPosition ||
!maskLayerRef.current ||
!shouldLockBoundingBox
)
return;
/** /**
* Extend the current line. * Extend the current line.
@ -185,13 +174,23 @@ const InpaintingCanvas = () => {
didMouseMoveRef.current = false; didMouseMoveRef.current = false;
} }
dispatch(setIsDrawing(false)); dispatch(setIsDrawing(false));
}, [dispatch, isDrawing]); }, [dispatch, isDrawing, shouldLockBoundingBox]);
/**
*
* Canvas onMouseOut
*
*/
const handleMouseOutCanvas = useCallback(() => { const handleMouseOutCanvas = useCallback(() => {
dispatch(setCursorPosition(null)); dispatch(setCursorPosition(null));
dispatch(setIsDrawing(false)); dispatch(setIsDrawing(false));
}, [dispatch]); }, [dispatch]);
/**
*
* Canvas onMouseEnter
*
*/
const handleMouseEnter = useCallback( const handleMouseEnter = useCallback(
(e: KonvaEventObject<MouseEvent>) => { (e: KonvaEventObject<MouseEvent>) => {
if (e.evt.buttons === 1) { if (e.evt.buttons === 1) {
@ -202,8 +201,7 @@ const InpaintingCanvas = () => {
if ( if (
!scaledCursorPosition || !scaledCursorPosition ||
!maskLayerRef.current || !maskLayerRef.current ||
isMovingBoundingBox || !shouldLockBoundingBox
isTransformingBoundingBox
) )
return; return;
@ -219,7 +217,7 @@ const InpaintingCanvas = () => {
); );
} }
}, },
[dispatch, brushSize, tool, isMovingBoundingBox, isTransformingBoundingBox] [dispatch, brushSize, tool, shouldLockBoundingBox]
); );
return ( return (

View File

@ -1,9 +1,10 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import Konva from 'konva'; import Konva from 'konva';
import { KonvaEventObject } from 'konva/lib/Node'; import { KonvaEventObject } from 'konva/lib/Node';
import { Box } from 'konva/lib/shapes/Transformer';
import { Vector2d } from 'konva/lib/types'; import { Vector2d } from 'konva/lib/types';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useLayoutEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Group, Rect, Transformer } from 'react-konva'; import { Group, Rect, Transformer } from 'react-konva';
import { import {
RootState, RootState,
@ -15,9 +16,6 @@ import {
InpaintingState, InpaintingState,
setBoundingBoxCoordinate, setBoundingBoxCoordinate,
setBoundingBoxDimensions, setBoundingBoxDimensions,
setIsTransformingBoundingBox,
setIsDrawing,
setShouldShowBrush,
} from '../inpaintingSlice'; } from '../inpaintingSlice';
import { rgbaColorToString } from '../util/colorToString'; import { rgbaColorToString } from '../util/colorToString';
import { import {
@ -35,7 +33,6 @@ const boundingBoxPreviewSelector = createSelector(
canvasDimensions, canvasDimensions,
stageScale, stageScale,
imageToInpaint, imageToInpaint,
isMovingBoundingBox,
shouldLockBoundingBox, shouldLockBoundingBox,
} = inpainting; } = inpainting;
return { return {
@ -47,7 +44,6 @@ const boundingBoxPreviewSelector = createSelector(
imageToInpaint, imageToInpaint,
dash: DASH_WIDTH / stageScale, // scale dash lengths dash: DASH_WIDTH / stageScale, // scale dash lengths
strokeWidth: 1 / stageScale, // scale stroke thickness strokeWidth: 1 / stageScale, // scale stroke thickness
isMovingBoundingBox,
shouldLockBoundingBox, shouldLockBoundingBox,
}; };
}, },
@ -173,29 +169,31 @@ const InpaintingBoundingBoxPreview = () => {
const scaledStep = 64 * stageScale; const scaledStep = 64 * stageScale;
console.log(shouldLockBoundingBox); const handleOnDragMove = useCallback(
(e: KonvaEventObject<DragEvent>) => {
dispatch(setBoundingBoxCoordinate({ x: e.target.x(), y: e.target.y() }));
},
[dispatch]
);
return ( const dragBoundFunc = useCallback(
<> (position: Vector2d) => {
<Rect if (!imageToInpaint) return boundingBoxCoordinate;
x={boundingBoxCoordinate.x}
y={boundingBoxCoordinate.y} const { x, y } = position;
width={boundingBoxDimensions.width}
height={boundingBoxDimensions.height} const maxX = imageToInpaint.width - boundingBoxDimensions.width;
ref={shapeRef} const maxY = imageToInpaint.height - boundingBoxDimensions.height;
stroke={'white'}
strokeWidth={strokeWidth} const clampedX = _.clamp(x, 0, maxX);
listening={false} const clampedY = _.clamp(y, 0, maxY);
onTransformStart={() => {
dispatch(setIsDrawing(false)); return { x: clampedX, y: clampedY };
dispatch(setShouldShowBrush(false)); },
dispatch(setIsTransformingBoundingBox(true)); [boundingBoxCoordinate, boundingBoxDimensions, imageToInpaint]
}} );
onTransformEnd={() => {
dispatch(setShouldShowBrush(true)); const handleOnTransform = useCallback(() => {
dispatch(setIsTransformingBoundingBox(false));
}}
onTransform={() => {
/** /**
* The Konva Transformer changes the object's anchor point and scale factor, * 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 * not its width and height. We need to un-scale the width and height before
@ -232,29 +230,10 @@ const InpaintingBoundingBoxPreview = () => {
// Reset the scale now that the coords/dimensions have been un-scaled // Reset the scale now that the coords/dimensions have been un-scaled
rect.scaleX(1); rect.scaleX(1);
rect.scaleY(1); rect.scaleY(1);
}} }, [dispatch]);
/>
<Transformer const anchorDragBoundFunc = useCallback(
enabledAnchors={shouldLockBoundingBox ? [] : undefined} (
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;
}}
onMouseOver={(e: KonvaEventObject<MouseEvent>) => {
e.cancelBubble = true;
}}
anchorDragBoundFunc={(
oldPos: Vector2d, // old absolute position of anchor point oldPos: Vector2d, // old absolute position of anchor point
newPos: Vector2d, // new absolute position (potentially) of anchor point newPos: Vector2d, // new absolute position (potentially) of anchor point
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -307,8 +286,12 @@ const InpaintingBoundingBoxPreview = () => {
} }
return newCoordinate; return newCoordinate;
}} },
boundBoxFunc={(oldBoundBox, newBoundBox) => { [scaledStep]
);
const boundBoxFunc = useCallback(
(oldBoundBox: Box, newBoundBox: Box) => {
/** /**
* The transformer uses this callback to limit valid transformations. * The transformer uses this callback to limit valid transformations.
* Unlike anchorDragBoundFunc, it does get a width and height, so * Unlike anchorDragBoundFunc, it does get a width and height, so
@ -317,8 +300,7 @@ const InpaintingBoundingBoxPreview = () => {
if (!imageToInpaint) return oldBoundBox; if (!imageToInpaint) return oldBoundBox;
if ( if (
newBoundBox.width + newBoundBox.x > newBoundBox.width + newBoundBox.x > imageToInpaint.width * stageScale ||
imageToInpaint.width * stageScale ||
newBoundBox.height + newBoundBox.y > newBoundBox.height + newBoundBox.y >
imageToInpaint.height * stageScale || imageToInpaint.height * stageScale ||
newBoundBox.x < 0 || newBoundBox.x < 0 ||
@ -328,7 +310,54 @@ const InpaintingBoundingBoxPreview = () => {
} }
return newBoundBox; return newBoundBox;
},
[imageToInpaint, stageScale]
);
return (
<>
<Rect
x={boundingBoxCoordinate.x}
y={boundingBoxCoordinate.y}
width={boundingBoxDimensions.width}
height={boundingBoxDimensions.height}
ref={shapeRef}
stroke={'white'}
strokeWidth={strokeWidth}
listening={!shouldLockBoundingBox}
onMouseEnter={(e) => {
// style stage container:
const container = e?.target?.getStage()?.container();
if (!container) return;
container.style.cursor = 'move';
}} }}
onMouseLeave={(e) => {
const container = e?.target?.getStage()?.container();
if (!container) return;
container.style.cursor = 'default';
}}
draggable={!shouldLockBoundingBox}
onDragMove={handleOnDragMove}
dragBoundFunc={dragBoundFunc}
onTransform={handleOnTransform}
/>
<Transformer
ref={transformerRef}
anchorCornerRadius={3}
anchorFill={'rgba(212,216,234,1)'}
anchorSize={15}
anchorStroke={'rgb(42,42,42)'}
borderDash={[4, 4]}
borderStroke={'black'}
rotateEnabled={false}
borderEnabled={true}
flipEnabled={false}
ignoreStroke={true}
keepRatio={false}
listening={!shouldLockBoundingBox}
enabledAnchors={shouldLockBoundingBox ? [] : undefined}
boundBoxFunc={boundBoxFunc}
anchorDragBoundFunc={anchorDragBoundFunc}
/> />
</> </>
); );

View File

@ -10,7 +10,9 @@ import { OptionsState } from '../../../options/optionsSlice';
import { tabMap } from '../../InvokeTabs'; import { tabMap } from '../../InvokeTabs';
import { import {
InpaintingState, InpaintingState,
setIsMovingBoundingBox, setIsDrawing,
setShouldLockBoundingBox,
setShouldShowBrush,
toggleTool, toggleTool,
} from '../inpaintingSlice'; } from '../inpaintingSlice';
@ -20,14 +22,12 @@ const keyboardEventManagerSelector = createSelector(
const { const {
shouldShowMask, shouldShowMask,
cursorPosition, cursorPosition,
isMovingBoundingBox,
shouldLockBoundingBox, shouldLockBoundingBox,
} = inpainting; } = inpainting;
return { return {
activeTabName: tabMap[options.activeTab], activeTabName: tabMap[options.activeTab],
shouldShowMask, shouldShowMask,
isCursorOnCanvas: Boolean(cursorPosition), isCursorOnCanvas: Boolean(cursorPosition),
isMovingBoundingBox,
shouldLockBoundingBox, shouldLockBoundingBox,
}; };
}, },
@ -44,7 +44,6 @@ const KeyboardEventManager = () => {
shouldShowMask, shouldShowMask,
activeTabName, activeTabName,
isCursorOnCanvas, isCursorOnCanvas,
isMovingBoundingBox,
shouldLockBoundingBox, shouldLockBoundingBox,
} = useAppSelector(keyboardEventManagerSelector); } = useAppSelector(keyboardEventManagerSelector);
@ -54,11 +53,9 @@ const KeyboardEventManager = () => {
useEffect(() => { useEffect(() => {
const listener = (e: KeyboardEvent) => { const listener = (e: KeyboardEvent) => {
if ( if (
!['Alt', ' '].includes(e.key) || !['z', ' '].includes(e.key) ||
activeTabName !== 'inpainting' || activeTabName !== 'inpainting' ||
!shouldShowMask || !shouldShowMask
e.repeat ||
shouldLockBoundingBox
) { ) {
return; return;
} }
@ -72,8 +69,10 @@ const KeyboardEventManager = () => {
wasLastEventOverCanvas.current = false; wasLastEventOverCanvas.current = false;
return; return;
} }
e.stopPropagation();
// cursor is over canvas e.preventDefault();
if (e.repeat) return;
// cursor is over canvas, we can preventDefault now
// if this is the first event // if this is the first event
if (!lastEvent.current) { if (!lastEvent.current) {
@ -87,15 +86,20 @@ const KeyboardEventManager = () => {
return; return;
} }
e.preventDefault();
switch (e.key) { switch (e.key) {
case 'Alt': { case 'z': {
dispatch(toggleTool()); dispatch(toggleTool());
break; break;
} }
case ' ': { case ' ': {
dispatch(setIsMovingBoundingBox(e.type === 'keydown' ? true : false)); if (e.type === 'keydown') {
dispatch(setIsDrawing(false));
dispatch(setShouldLockBoundingBox(false));
dispatch(setShouldShowBrush(false));
} else {
dispatch(setShouldLockBoundingBox(true));
dispatch(setShouldShowBrush(true));
}
break; break;
} }
} }
@ -116,7 +120,7 @@ const KeyboardEventManager = () => {
activeTabName, activeTabName,
shouldShowMask, shouldShowMask,
isCursorOnCanvas, isCursorOnCanvas,
isMovingBoundingBox, shouldLockBoundingBox,
]); ]);
return null; return null;

View File

@ -291,7 +291,19 @@ export const inpaintingSlice = createSlice({
}; };
}, },
setBoundingBoxCoordinate: (state, action: PayloadAction<Vector2d>) => { setBoundingBoxCoordinate: (state, action: PayloadAction<Vector2d>) => {
state.boundingBoxCoordinate = action.payload; state.boundingBoxCoordinate = action.payload
// const { x, y } = action.payload;
// const maxX =
// state.canvasDimensions.width - state.boundingBoxDimensions.width;
// const maxY =
// state.canvasDimensions.height - state.boundingBoxDimensions.height;
// const clampedX = _.clamp(x, 0, maxX);
// const clampedY = _.clamp(y, 0, maxY);
// state.boundingBoxCoordinate = { x: clampedX, y: clampedY };
}, },
setIsMovingBoundingBox: (state, action: PayloadAction<boolean>) => { setIsMovingBoundingBox: (state, action: PayloadAction<boolean>) => {
state.isMovingBoundingBox = action.payload; state.isMovingBoundingBox = action.payload;

View File

@ -69,35 +69,26 @@ export const inpaintingCanvasSelector = createSelector(
shouldInvertMask, shouldInvertMask,
shouldShowMask, shouldShowMask,
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
shouldShowBrushPreview,
imageToInpaint, imageToInpaint,
isMovingBoundingBox,
boundingBoxDimensions,
canvasDimensions,
boundingBoxCoordinate,
stageScale, stageScale,
shouldShowBoundingBoxFill, shouldShowBoundingBoxFill,
isDrawing, isDrawing,
isTransformingBoundingBox, shouldLockBoundingBox,
shouldShowBoundingBox, shouldShowBoundingBox,
} = inpainting; } = inpainting;
return { return {
tool, tool,
brushSize, brushSize,
maskColor,
shouldInvertMask, shouldInvertMask,
shouldShowMask, shouldShowMask,
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
shouldShowBrushPreview, maskColor,
imageToInpaint, imageToInpaint,
isMovingBoundingBox,
boundingBoxDimensions,
canvasDimensions,
boundingBoxCoordinate,
stageScale, stageScale,
shouldShowBoundingBoxFill, shouldShowBoundingBoxFill,
isDrawing, isDrawing,
isTransformingBoundingBox, shouldLockBoundingBox,
shouldShowBoundingBox, shouldShowBoundingBox,
}; };
}, },