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',
desc: 'Quick toggle between brush and eraser',
hotkey: 'Alt',
hotkey: 'Z',
},
{
title: 'Decrease Brush Size',

View File

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

View File

@ -1,9 +1,10 @@
import { createSelector } from '@reduxjs/toolkit';
import Konva from 'konva';
import { KonvaEventObject } from 'konva/lib/Node';
import { Box } from 'konva/lib/shapes/Transformer';
import { Vector2d } from 'konva/lib/types';
import _ from 'lodash';
import { useEffect, useLayoutEffect, useRef } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Group, Rect, Transformer } from 'react-konva';
import {
RootState,
@ -15,9 +16,6 @@ import {
InpaintingState,
setBoundingBoxCoordinate,
setBoundingBoxDimensions,
setIsTransformingBoundingBox,
setIsDrawing,
setShouldShowBrush,
} from '../inpaintingSlice';
import { rgbaColorToString } from '../util/colorToString';
import {
@ -35,7 +33,6 @@ const boundingBoxPreviewSelector = createSelector(
canvasDimensions,
stageScale,
imageToInpaint,
isMovingBoundingBox,
shouldLockBoundingBox,
} = inpainting;
return {
@ -47,7 +44,6 @@ const boundingBoxPreviewSelector = createSelector(
imageToInpaint,
dash: DASH_WIDTH / stageScale, // scale dash lengths
strokeWidth: 1 / stageScale, // scale stroke thickness
isMovingBoundingBox,
shouldLockBoundingBox,
};
},
@ -173,29 +169,31 @@ const InpaintingBoundingBoxPreview = () => {
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 (
<>
<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(setIsTransformingBoundingBox(true));
}}
onTransformEnd={() => {
dispatch(setShouldShowBrush(true));
dispatch(setIsTransformingBoundingBox(false));
}}
onTransform={() => {
const dragBoundFunc = useCallback(
(position: Vector2d) => {
if (!imageToInpaint) return boundingBoxCoordinate;
const { x, y } = position;
const maxX = imageToInpaint.width - boundingBoxDimensions.width;
const maxY = imageToInpaint.height - boundingBoxDimensions.height;
const clampedX = _.clamp(x, 0, maxX);
const clampedY = _.clamp(y, 0, maxY);
return { x: clampedX, y: clampedY };
},
[boundingBoxCoordinate, boundingBoxDimensions, imageToInpaint]
);
const handleOnTransform = useCallback(() => {
/**
* 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
@ -232,29 +230,10 @@ const InpaintingBoundingBoxPreview = () => {
// Reset the scale now that the coords/dimensions have been un-scaled
rect.scaleX(1);
rect.scaleY(1);
}}
/>
<Transformer
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={(
}, [dispatch]);
const anchorDragBoundFunc = useCallback(
(
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
@ -307,8 +286,12 @@ const InpaintingBoundingBoxPreview = () => {
}
return newCoordinate;
}}
boundBoxFunc={(oldBoundBox, newBoundBox) => {
},
[scaledStep]
);
const boundBoxFunc = useCallback(
(oldBoundBox: Box, newBoundBox: Box) => {
/**
* The transformer uses this callback to limit valid transformations.
* Unlike anchorDragBoundFunc, it does get a width and height, so
@ -317,8 +300,7 @@ const InpaintingBoundingBoxPreview = () => {
if (!imageToInpaint) return oldBoundBox;
if (
newBoundBox.width + newBoundBox.x >
imageToInpaint.width * stageScale ||
newBoundBox.width + newBoundBox.x > imageToInpaint.width * stageScale ||
newBoundBox.height + newBoundBox.y >
imageToInpaint.height * stageScale ||
newBoundBox.x < 0 ||
@ -328,7 +310,54 @@ const InpaintingBoundingBoxPreview = () => {
}
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 {
InpaintingState,
setIsMovingBoundingBox,
setIsDrawing,
setShouldLockBoundingBox,
setShouldShowBrush,
toggleTool,
} from '../inpaintingSlice';
@ -20,14 +22,12 @@ const keyboardEventManagerSelector = createSelector(
const {
shouldShowMask,
cursorPosition,
isMovingBoundingBox,
shouldLockBoundingBox,
} = inpainting;
return {
activeTabName: tabMap[options.activeTab],
shouldShowMask,
isCursorOnCanvas: Boolean(cursorPosition),
isMovingBoundingBox,
shouldLockBoundingBox,
};
},
@ -44,7 +44,6 @@ const KeyboardEventManager = () => {
shouldShowMask,
activeTabName,
isCursorOnCanvas,
isMovingBoundingBox,
shouldLockBoundingBox,
} = useAppSelector(keyboardEventManagerSelector);
@ -54,11 +53,9 @@ const KeyboardEventManager = () => {
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (
!['Alt', ' '].includes(e.key) ||
!['z', ' '].includes(e.key) ||
activeTabName !== 'inpainting' ||
!shouldShowMask ||
e.repeat ||
shouldLockBoundingBox
!shouldShowMask
) {
return;
}
@ -72,8 +69,10 @@ const KeyboardEventManager = () => {
wasLastEventOverCanvas.current = false;
return;
}
// cursor is over canvas
e.stopPropagation();
e.preventDefault();
if (e.repeat) return;
// cursor is over canvas, we can preventDefault now
// if this is the first event
if (!lastEvent.current) {
@ -87,15 +86,20 @@ const KeyboardEventManager = () => {
return;
}
e.preventDefault();
switch (e.key) {
case 'Alt': {
case 'z': {
dispatch(toggleTool());
break;
}
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;
}
}
@ -116,7 +120,7 @@ const KeyboardEventManager = () => {
activeTabName,
shouldShowMask,
isCursorOnCanvas,
isMovingBoundingBox,
shouldLockBoundingBox,
]);
return null;

View File

@ -291,7 +291,19 @@ export const inpaintingSlice = createSlice({
};
},
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>) => {
state.isMovingBoundingBox = action.payload;

View File

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