diff --git a/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx b/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx index d1b33e604d..e3cfe8f51a 100644 --- a/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx +++ b/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx @@ -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', diff --git a/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx b/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx index 06481119b1..5d55fb341b 100644 --- a/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx +++ b/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx @@ -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) => { 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 ( diff --git a/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx b/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx index ebba337065..fb2150d087 100644 --- a/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx +++ b/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx @@ -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) => { + 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} /> ) => { - e.cancelBubble = true; - }} - onMouseOver={(e: KonvaEventObject) => { - 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} /> ); diff --git a/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx b/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx index c087cb08e7..8f1d84631b 100644 --- a/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx +++ b/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx @@ -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; diff --git a/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts b/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts index 94d2c5ce25..8a24e068f4 100644 --- a/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts +++ b/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts @@ -291,7 +291,19 @@ export const inpaintingSlice = createSlice({ }; }, setBoundingBoxCoordinate: (state, action: PayloadAction) => { - 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) => { state.isMovingBoundingBox = action.payload; diff --git a/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts b/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts index 634bb4077d..bd477ffaab 100644 --- a/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts +++ b/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts @@ -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, }; },