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 ( ); }; // /** // * Draws marching ants around the mask. Unused. // */ // const _InpaintingBoundingBoxPreviewMarchingAnts = () => { // const { boundingBoxCoordinate, boundingBoxDimensions } = useAppSelector( // boundingBoxPreviewSelector // ); // const blackStrokeRectRef = useRef(null); // const whiteStrokeRectRef = useRef(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 ( // // // // // ); // }; const InpaintingBoundingBoxPreview = () => { const dispatch = useAppDispatch(); const { boundingBoxCoordinate, boundingBoxDimensions, strokeWidth, stageScale, imageToInpaint, } = useAppSelector(boundingBoxPreviewSelector); const transformerRef = useRef(null); const shapeRef = useRef(null); useEffect(() => { if (!transformerRef.current || !shapeRef.current) return; transformerRef.current.nodes([shapeRef.current]); transformerRef.current.getLayer()?.batchDraw(); }, []); const scaledStep = 64 * stageScale; return ( <> { 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); }} /> ) => { 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;