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,69 +111,55 @@ const InpaintingCanvas = () => {
points: [scaledCursorPosition.x, scaledCursorPosition.y],
})
);
}, [dispatch, brushSize, tool]);
}, [dispatch, brushSize, tool, shouldLockBoundingBox]);
const handleMouseMove = useCallback(() => {
if (!stageRef.current) return;
/**
*
* Canvas onMouseMove
*
*/
const handleMouseMove = useCallback(
() => {
if (!stageRef.current) return;
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
if (!scaledCursorPosition) return;
if (!scaledCursorPosition) return;
dispatch(setCursorPosition(scaledCursorPosition));
dispatch(setCursorPosition(scaledCursorPosition));
if (!maskLayerRef.current) {
return;
}
if (!maskLayerRef.current || !shouldLockBoundingBox) {
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
);
if (!isDrawing) return;
didMouseMoveRef.current = true;
// Extend the current line
dispatch(
setBoundingBoxCoordinate({
x,
y,
})
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
);
},
[dispatch, isDrawing, shouldLockBoundingBox]
);
return;
}
if (!isDrawing) return;
didMouseMoveRef.current = true;
// Extend the current line
dispatch(
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
);
}, [
dispatch,
isMovingBoundingBox,
boundingBoxDimensions,
canvasDimensions,
boundingBoxCoordinate,
isDrawing,
]);
/**
*
* 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,7 +169,150 @@ 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]
);
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
* 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);
}, [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
_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;
},
[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
* 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;
},
[imageToInpaint, stageScale]
);
return (
<>
@ -185,150 +324,40 @@ const InpaintingBoundingBoxPreview = () => {
ref={shapeRef}
stroke={'white'}
strokeWidth={strokeWidth}
listening={false}
onTransformStart={() => {
dispatch(setIsDrawing(false));
dispatch(setShouldShowBrush(false));
dispatch(setIsTransformingBoundingBox(true));
listening={!shouldLockBoundingBox}
onMouseEnter={(e) => {
// style stage container:
const container = e?.target?.getStage()?.container();
if (!container) return;
container.style.cursor = 'move';
}}
onTransformEnd={() => {
dispatch(setShouldShowBrush(true));
dispatch(setIsTransformingBoundingBox(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);
onMouseLeave={(e) => {
const container = e?.target?.getStage()?.container();
if (!container) return;
container.style.cursor = 'default';
}}
draggable={!shouldLockBoundingBox}
onDragMove={handleOnDragMove}
dragBoundFunc={dragBoundFunc}
onTransform={handleOnTransform}
/>
<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}
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}
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
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;
}}
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,
};
},