From 8ab428e5880dc408cf41ec10de80bdad06f5f437 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 27 Oct 2022 16:50:27 +1100
Subject: [PATCH] Adds bounding box handles

---
 .../src/common/util/roundDownToMultiple.ts    |   4 +
 .../tabs/Inpainting/InpaintingCanvas.tsx      |  18 +-
 .../tabs/Inpainting/InpaintingControls.tsx    |  72 ++--
 .../InpaintingBoundingBoxPreview.tsx          | 335 ++++++++++++------
 .../InpaintingCanvasBrushPreview.tsx          |   4 +-
 .../components/InpaintingCanvasLines.tsx      |   9 +-
 .../components/KeyboardEventManager.tsx       |  41 ++-
 .../tabs/Inpainting/inpaintingSlice.ts        | 123 ++++---
 .../Inpainting/inpaintingSliceSelectors.ts    |  20 +-
 .../util/boundingBoxAnchorBoundDragFunc.ts    |   0
 .../tabs/Inpainting/util/colorToString.ts     |   5 +
 .../tabs/Inpainting/util/constants.ts         |   5 +
 12 files changed, 426 insertions(+), 210 deletions(-)
 create mode 100644 frontend/src/features/tabs/Inpainting/util/boundingBoxAnchorBoundDragFunc.ts

diff --git a/frontend/src/common/util/roundDownToMultiple.ts b/frontend/src/common/util/roundDownToMultiple.ts
index 9143a014a6..850b235629 100644
--- a/frontend/src/common/util/roundDownToMultiple.ts
+++ b/frontend/src/common/util/roundDownToMultiple.ts
@@ -1,3 +1,7 @@
 export const roundDownToMultiple = (num: number, multiple: number): number => {
   return Math.floor(num / multiple) * multiple;
 };
+
+export const roundToMultiple = (num: number, multiple: number): number => {
+  return Math.round(num / multiple) * multiple;
+};
diff --git a/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx b/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx
index 81a4bc1a0a..840da4d446 100644
--- a/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx
+++ b/frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx
@@ -1,6 +1,5 @@
 // lib
 import {
-  KeyboardEvent,
   MutableRefObject,
   useCallback,
   useEffect,
@@ -20,7 +19,6 @@ import {
   setBoundingBoxCoordinate,
   setCursorPosition,
   setIsMovingBoundingBox,
-  setTool,
 } from './inpaintingSlice';
 import { inpaintingCanvasSelector } from './inpaintingSliceSelectors';
 
@@ -32,7 +30,9 @@ import Cacher from './components/Cacher';
 import { Vector2d } from 'konva/lib/types';
 import getScaledCursorPosition from './util/getScaledCursorPosition';
 import _ from 'lodash';
-import InpaintingBoundingBoxPreview from './components/InpaintingBoundingBoxPreview';
+import InpaintingBoundingBoxPreview, {
+  InpaintingBoundingBoxPreviewOverlay,
+} from './components/InpaintingBoundingBoxPreview';
 import { KonvaEventObject } from 'konva/lib/Node';
 import KeyboardEventManager from './components/KeyboardEventManager';
 
@@ -50,13 +50,14 @@ const InpaintingCanvas = () => {
     shouldInvertMask,
     shouldShowMask,
     shouldShowCheckboardTransparency,
-    maskOpacity,
+    maskColor,
     imageToInpaint,
     isMovingBoundingBox,
     boundingBoxDimensions,
     canvasDimensions,
     boundingBoxCoordinate,
     stageScale,
+    shouldShowBoundingBoxFill,
   } = useAppSelector(inpaintingCanvasSelector);
 
   // set the closure'd refs
@@ -248,7 +249,7 @@ const InpaintingCanvas = () => {
                 opacity={
                   shouldShowCheckboardTransparency || shouldInvertMask
                     ? 1
-                    : maskOpacity
+                    : maskColor.a
                 }
                 ref={maskLayerRef}
               >
@@ -270,8 +271,11 @@ const InpaintingCanvas = () => {
                   />
                 )}
               </Layer>
-              <Layer name={'preview-layer'} listening={false}>
+              <Layer name={'preview-layer'}>
                 <InpaintingCanvasBrushPreviewOutline />
+                {shouldShowBoundingBoxFill && (
+                  <InpaintingBoundingBoxPreviewOverlay />
+                )}
                 <InpaintingBoundingBoxPreview />
               </Layer>
             </>
@@ -284,6 +288,4 @@ const InpaintingCanvas = () => {
   );
 };
 
-// </div>
-
 export default InpaintingCanvas;
diff --git a/frontend/src/features/tabs/Inpainting/InpaintingControls.tsx b/frontend/src/features/tabs/Inpainting/InpaintingControls.tsx
index 8028c887f6..f7d510b4f6 100644
--- a/frontend/src/features/tabs/Inpainting/InpaintingControls.tsx
+++ b/frontend/src/features/tabs/Inpainting/InpaintingControls.tsx
@@ -6,32 +6,29 @@ import {
   FaPalette,
   FaPlus,
   FaRedo,
+  FaTint,
+  FaTintSlash,
   FaUndo,
 } from 'react-icons/fa';
 import { BiHide, BiShow } from 'react-icons/bi';
 import { VscSplitHorizontal } from 'react-icons/vsc';
-import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
+import { useAppDispatch, useAppSelector } from '../../../app/store';
 import IAIIconButton from '../../../common/components/IAIIconButton';
 import {
   clearMask,
   redo,
   setMaskColor,
   setBrushSize,
-  setMaskOpacity,
   setShouldShowBrushPreview,
   setTool,
   undo,
   setShouldShowMask,
   setShouldInvertMask,
   setNeedsRepaint,
+  setShouldShowBoundingBoxFill,
 } from './inpaintingSlice';
 
-import { tabMap } from '../InvokeTabs';
-import {
-  MdInvertColors,
-  MdInvertColorsOff,
-  MdOutlineCloseFullscreen,
-} from 'react-icons/md';
+import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
 import IAISlider from '../../../common/components/IAISlider';
 import IAINumberInput from '../../../common/components/IAINumberInput';
 import { inpaintingControlsSelector } from './inpaintingSliceSelectors';
@@ -39,14 +36,12 @@ import IAIPopover from '../../../common/components/IAIPopover';
 import IAIColorPicker from '../../../common/components/IAIColorPicker';
 import { RgbaColor } from 'react-colorful';
 import { setShowDualDisplay } from '../../options/optionsSlice';
-import { useEffect } from 'react';
 
 const InpaintingControls = () => {
   const {
     tool,
     brushSize,
     maskColor,
-    maskOpacity,
     shouldInvertMask,
     shouldShowMask,
     canUndo,
@@ -54,12 +49,17 @@ const InpaintingControls = () => {
     isMaskEmpty,
     activeTabName,
     showDualDisplay,
+    shouldShowBoundingBoxFill,
   } = useAppSelector(inpaintingControlsSelector);
 
   const dispatch = useAppDispatch();
   const toast = useToast();
 
-  // Hotkeys
+  /**
+   * Hotkeys
+   */
+
+  // Decrease brush size
   useHotkeys(
     '[',
     (e: KeyboardEvent) => {
@@ -76,6 +76,7 @@ const InpaintingControls = () => {
     [activeTabName, shouldShowMask, brushSize]
   );
 
+  // Increase brush size
   useHotkeys(
     ']',
     (e: KeyboardEvent) => {
@@ -88,30 +89,39 @@ const InpaintingControls = () => {
     [activeTabName, shouldShowMask, brushSize]
   );
 
+  // Decrease mask opacity
   useHotkeys(
     'shift+[',
     (e: KeyboardEvent) => {
       e.preventDefault();
-      handleChangeMaskOpacity(Math.max(maskOpacity - 0.05, 0));
+      handleChangeMaskColor({
+        ...maskColor,
+        a: Math.max(maskColor.a - 0.05, 0),
+      });
     },
     {
       enabled: activeTabName === 'inpainting' && shouldShowMask,
     },
-    [activeTabName, shouldShowMask, maskOpacity]
+    [activeTabName, shouldShowMask, maskColor.a]
   );
 
+  // Increase mask opacity
   useHotkeys(
     'shift+]',
     (e: KeyboardEvent) => {
       e.preventDefault();
-      handleChangeMaskOpacity(Math.min(maskOpacity + 0.05, 100));
+      handleChangeMaskColor({
+        ...maskColor,
+        a: Math.min(maskColor.a + 0.05, 100),
+      });
     },
     {
       enabled: activeTabName === 'inpainting' && shouldShowMask,
     },
-    [activeTabName, shouldShowMask, maskOpacity]
+    [activeTabName, shouldShowMask, maskColor.a]
   );
 
+  // Set tool to eraser
   useHotkeys(
     'e',
     (e: KeyboardEvent) => {
@@ -125,6 +135,7 @@ const InpaintingControls = () => {
     [activeTabName, shouldShowMask]
   );
 
+  // Set tool to brush
   useHotkeys(
     'b',
     (e: KeyboardEvent) => {
@@ -137,6 +148,7 @@ const InpaintingControls = () => {
     [activeTabName, shouldShowMask]
   );
 
+  // Undo
   useHotkeys(
     'cmd+z, control+z',
     (e: KeyboardEvent) => {
@@ -149,6 +161,7 @@ const InpaintingControls = () => {
     [activeTabName, shouldShowMask, canUndo]
   );
 
+  // Redo
   useHotkeys(
     'cmd+shift+z, control+shift+z, control+y, cmd+y',
     (e: KeyboardEvent) => {
@@ -161,6 +174,7 @@ const InpaintingControls = () => {
     [activeTabName, shouldShowMask, canRedo]
   );
 
+  // Show/hide mask
   useHotkeys(
     'h',
     (e: KeyboardEvent) => {
@@ -173,6 +187,7 @@ const InpaintingControls = () => {
     [activeTabName, shouldShowMask]
   );
 
+  // Invert mask
   useHotkeys(
     'shift+m',
     (e: KeyboardEvent) => {
@@ -185,6 +200,7 @@ const InpaintingControls = () => {
     [activeTabName, shouldInvertMask, shouldShowMask]
   );
 
+  // Clear mask
   useHotkeys(
     'shift+c',
     (e: KeyboardEvent) => {
@@ -203,6 +219,7 @@ const InpaintingControls = () => {
     [activeTabName, isMaskEmpty, shouldShowMask]
   );
 
+  // Toggle split view
   useHotkeys(
     'shift+j',
     () => {
@@ -224,10 +241,6 @@ const InpaintingControls = () => {
     dispatch(setBrushSize(v));
   };
 
-  const handleChangeMaskOpacity = (v: number) => {
-    dispatch(setMaskOpacity(v));
-  };
-
   const handleToggleShouldShowMask = () =>
     dispatch(setShouldShowMask(!shouldShowMask));
 
@@ -242,10 +255,8 @@ const InpaintingControls = () => {
     dispatch(setShouldShowBrushPreview(false));
   };
 
-  const handleChangeBrushColor = (newColor: RgbaColor) => {
-    const { r, g, b, a: maskOpacity } = newColor;
-    dispatch(setMaskColor({ r, g, b }));
-    dispatch(setMaskOpacity(maskOpacity));
+  const handleChangeMaskColor = (newColor: RgbaColor) => {
+    dispatch(setMaskColor(newColor));
   };
 
   const handleUndo = () => dispatch(undo());
@@ -257,6 +268,10 @@ const InpaintingControls = () => {
     dispatch(setNeedsRepaint(true));
   };
 
+  const handleChangeShouldShowBoundingBoxFill = () => {
+    dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
+  };
+
   return (
     <div className="inpainting-settings">
       <div className="inpainting-buttons">
@@ -320,8 +335,8 @@ const InpaintingControls = () => {
             }
           >
             <IAIColorPicker
-              color={{ ...maskColor, a: maskOpacity }}
-              onChange={handleChangeBrushColor}
+              color={maskColor}
+              onChange={handleChangeMaskColor}
             />
           </IAIPopover>
           <IAIIconButton
@@ -375,6 +390,13 @@ const InpaintingControls = () => {
             data-selected={showDualDisplay}
             onClick={handleDualDisplay}
           />
+          <IAIIconButton
+            aria-label="Darken Outside Bounding Box (xxx)"
+            tooltip="Darken Outside Bounding Box (xxx)"
+            icon={shouldShowBoundingBoxFill ? <FaTint /> : <FaTintSlash />}
+            data-selected={shouldShowBoundingBoxFill}
+            onClick={handleChangeShouldShowBoundingBoxFill}
+          />
         </div>
       </div>
     </div>
diff --git a/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx b/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx
index 1a2b28ce4a..d469542c6e 100644
--- a/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx
+++ b/frontend/src/features/tabs/Inpainting/components/InpaintingBoundingBoxPreview.tsx
@@ -1,13 +1,26 @@
 import { createSelector } from '@reduxjs/toolkit';
 import Konva from 'konva';
+import { Vector2d } from 'konva/lib/types';
 import _ from 'lodash';
 import { useEffect, useRef } from 'react';
-import { Group, Rect } from 'react-konva';
-import { RootState, useAppSelector } from '../../../../app/store';
-import { InpaintingState } from '../inpaintingSlice';
+import { Group, Rect, Transformer } from 'react-konva';
+import {
+  RootState,
+  useAppDispatch,
+  useAppSelector,
+} from '../../../../app/store';
+import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
+import {
+  InpaintingState,
+  setBoundingBoxCoordinate,
+  setBoundingBoxDimensions,
+} from '../inpaintingSlice';
 import { rgbaColorToString } from '../util/colorToString';
-import { DASH_WIDTH, MARCHING_ANTS_SPEED } from '../util/constants';
-
+import {
+  DASH_WIDTH,
+  // MARCHING_ANTS_SPEED,
+  TRANSFORMER_ANCHOR_SIZE,
+} from '../util/constants';
 
 const boundingBoxPreviewSelector = createSelector(
   (state: RootState) => state.inpainting,
@@ -18,14 +31,18 @@ const boundingBoxPreviewSelector = createSelector(
       boundingBoxPreviewFill,
       canvasDimensions,
       stageScale,
+      imageToInpaint,
     } = inpainting;
     return {
       boundingBoxCoordinate,
       boundingBoxDimensions,
       boundingBoxPreviewFillString: rgbaColorToString(boundingBoxPreviewFill),
       canvasDimensions,
-      dash: DASH_WIDTH / stageScale,  // scale dash lengths
+      stageScale,
+      imageToInpaint,
+      dash: DASH_WIDTH / stageScale, // scale dash lengths
       strokeWidth: 1 / stageScale, // scale stroke thickness
+      anchorSize: TRANSFORMER_ANCHOR_SIZE,
     };
   },
   {
@@ -38,7 +55,7 @@ const boundingBoxPreviewSelector = createSelector(
 /**
  * Shades the area around the mask.
  */
-const InpaintingBoundingBoxPreviewOverlay = () => {
+export const InpaintingBoundingBoxPreviewOverlay = () => {
   const {
     boundingBoxCoordinate,
     boundingBoxDimensions,
@@ -68,122 +85,228 @@ const InpaintingBoundingBoxPreviewOverlay = () => {
   );
 };
 
-/**
- * Draws marching ants around the mask.
- */
-const InpaintingBoundingBoxPreviewMarchingAnts = () => {
-  const { boundingBoxCoordinate, boundingBoxDimensions } = useAppSelector(
-    boundingBoxPreviewSelector
-  );
+// /**
+//  * Draws marching ants around the mask. Unused.
+//  */
+// const _InpaintingBoundingBoxPreviewMarchingAnts = () => {
+//   const { boundingBoxCoordinate, boundingBoxDimensions } = useAppSelector(
+//     boundingBoxPreviewSelector
+//   );
 
-  const blackStrokeRectRef = useRef<Konva.Rect>(null);
-  const whiteStrokeRectRef = useRef<Konva.Rect>(null);
+//   const blackStrokeRectRef = useRef<Konva.Rect>(null);
+//   const whiteStrokeRectRef = useRef<Konva.Rect>(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 (
+//     <Group>
+//       <Rect
+//         x={boundingBoxCoordinate.x}
+//         y={boundingBoxCoordinate.y}
+//         width={boundingBoxDimensions.width}
+//         height={boundingBoxDimensions.height}
+//         stroke={'black'}
+//         strokeWidth={1}
+//         dash={[4, 4]}
+//         ref={blackStrokeRectRef}
+//         listening={false}
+//       />
+//       <Rect
+//         x={boundingBoxCoordinate.x}
+//         y={boundingBoxCoordinate.y}
+//         width={boundingBoxDimensions.width}
+//         height={boundingBoxDimensions.height}
+//         stroke={'white'}
+//         dash={[4, 4]}
+//         strokeWidth={1}
+//         ref={whiteStrokeRectRef}
+//         listening={false}
+//       />
+//     </Group>
+//   );
+// };
+
+const InpaintingBoundingBoxPreview = () => {
+  const dispatch = useAppDispatch();
+  const {
+    boundingBoxCoordinate,
+    boundingBoxDimensions,
+    strokeWidth,
+    anchorSize,
+    stageScale,
+    imageToInpaint,
+  } = useAppSelector(boundingBoxPreviewSelector);
+
+  const transformerRef = useRef<Konva.Transformer>(null);
+  const shapeRef = useRef<Konva.Rect>(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();
-    };
+    if (!transformerRef.current || !shapeRef.current) return;
+    transformerRef.current.nodes([shapeRef.current]);
+    transformerRef.current.getLayer()?.batchDraw();
   }, []);
 
   return (
-    <Group>
-      <Rect
-        x={boundingBoxCoordinate.x}
-        y={boundingBoxCoordinate.y}
-        width={boundingBoxDimensions.width}
-        height={boundingBoxDimensions.height}
-        stroke={'black'}
-        strokeWidth={1}
-        dash={[4, 4]}
-        ref={blackStrokeRectRef}
-        listening={false}
-      />
+    <>
       <Rect
         x={boundingBoxCoordinate.x}
         y={boundingBoxCoordinate.y}
         width={boundingBoxDimensions.width}
         height={boundingBoxDimensions.height}
+        ref={shapeRef}
         stroke={'white'}
-        dash={[4, 4]}
-        strokeWidth={1}
-        ref={whiteStrokeRectRef}
-        listening={false}
-      />
-    </Group>
-  );
-};
-
-/**
- * Draws non-marching ants around the mask.
- */
-const InpaintingBoundingBoxPreviewAnts = () => {
-  const { boundingBoxCoordinate, boundingBoxDimensions, dash, strokeWidth } =
-    useAppSelector(boundingBoxPreviewSelector);
-
-  return (
-    <Group>
-      <Rect
-        x={boundingBoxCoordinate.x}
-        y={boundingBoxCoordinate.y}
-        width={boundingBoxDimensions.width}
-        height={boundingBoxDimensions.height}
-        stroke={'black'}
         strokeWidth={strokeWidth}
-        dash={[dash, dash]}
-        dashOffset={0}
         listening={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);
+        }}
       />
-      <Rect
-        x={boundingBoxCoordinate.x}
-        y={boundingBoxCoordinate.y}
-        width={boundingBoxDimensions.width}
-        height={boundingBoxDimensions.height}
-        stroke={'white'}
-        dash={[dash, dash]}
-        strokeWidth={strokeWidth}
-        dashOffset={dash}
-        listening={false}
+      <Transformer
+        ref={transformerRef}
+        rotateEnabled={false}
+        anchorSize={anchorSize}
+        anchorStroke={'rgb(42,42,42)'}
+        borderEnabled={true}
+        borderStroke={'black'}
+        borderDash={[DASH_WIDTH, DASH_WIDTH]}
+        anchorCornerRadius={3}
+        ignoreStroke={true}
+        keepRatio={false}
+        flipEnabled={false}
+        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
+          _event: 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.
+           */
+
+          // Get the scaled step
+          const scaledStep = 64 * stageScale;
+
+          // 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 ||
+            newBoundBox.height + newBoundBox.y > imageToInpaint.height ||
+            newBoundBox.x < 0 ||
+            newBoundBox.y < 0
+          ) {
+            return oldBoundBox;
+          }
+
+          return newBoundBox;
+        }}
       />
-    </Group>
+    </>
   );
 };
 
-const boundingBoxPreviewTypeSelector = createSelector(
-  (state: RootState) => state.inpainting,
-  (inpainting: InpaintingState) => inpainting.boundingBoxPreviewType
-);
-
-const InpaintingBoundingBoxPreview = () => {
-  const boundingBoxPreviewType = useAppSelector(boundingBoxPreviewTypeSelector);
-
-  switch (boundingBoxPreviewType) {
-    case 'overlay': {
-      return <InpaintingBoundingBoxPreviewOverlay />;
-    }
-    case 'ants': {
-      return <InpaintingBoundingBoxPreviewAnts />;
-    }
-    case 'marchingAnts': {
-      return <InpaintingBoundingBoxPreviewMarchingAnts />;
-    }
-    default:
-      return null;
-  }
-};
-
 export default InpaintingBoundingBoxPreview;
diff --git a/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasBrushPreview.tsx b/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasBrushPreview.tsx
index 6f465a2860..37fc8c8920 100644
--- a/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasBrushPreview.tsx
+++ b/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasBrushPreview.tsx
@@ -3,7 +3,7 @@ import _ from 'lodash';
 import { Circle } from 'react-konva';
 import { RootState, useAppSelector } from '../../../../app/store';
 import { InpaintingState } from '../inpaintingSlice';
-import { rgbColorToString } from '../util/colorToString';
+import { rgbaColorToRgbString } from '../util/colorToString';
 
 const inpaintingCanvasBrushPreviewSelector = createSelector(
   (state: RootState) => state.inpainting,
@@ -23,7 +23,7 @@ const inpaintingCanvasBrushPreviewSelector = createSelector(
       height,
       shouldShowBrushPreview,
       brushSize,
-      maskColorString: rgbColorToString(maskColor),
+      maskColorString: rgbaColorToRgbString(maskColor),
       tool,
     };
   },
diff --git a/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasLines.tsx b/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasLines.tsx
index 13cc785c25..d9cc41038d 100644
--- a/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasLines.tsx
+++ b/frontend/src/features/tabs/Inpainting/components/InpaintingCanvasLines.tsx
@@ -1,5 +1,6 @@
 import { Line } from 'react-konva';
-import { RootState, useAppSelector } from '../../../../app/store';
+import { useAppSelector } from '../../../../app/store';
+import { inpaintingCanvasLinesSelector } from '../inpaintingSliceSelectors';
 
 /**
  * Draws the lines which comprise the mask.
@@ -7,11 +8,9 @@ import { RootState, useAppSelector } from '../../../../app/store';
  * Uses globalCompositeOperation to handle the brush and eraser tools.
  */
 const InpaintingCanvasLines = () => {
-  const { lines, maskColor } = useAppSelector(
-    (state: RootState) => state.inpainting
+  const { lines, maskColorString } = useAppSelector(
+    inpaintingCanvasLinesSelector
   );
-  const { r, g, b } = maskColor;
-  const maskColorString = `rgb(${r},${g},${b})`;
 
   return (
     <>
diff --git a/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx b/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx
index 5c42c16c96..4c20af3197 100644
--- a/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx
+++ b/frontend/src/features/tabs/Inpainting/components/KeyboardEventManager.tsx
@@ -39,24 +39,10 @@ const KeyboardEventManager = () => {
 
   const isFirstEvent = useRef<boolean>(true);
   const wasLastEventOverCanvas = useRef<boolean>(false);
+  const lastEvent = useRef<KeyboardEvent | null>(null);
 
   useEffect(() => {
     const listener = (e: KeyboardEvent) => {
-      if (!isCursorOnCanvas) {
-        wasLastEventOverCanvas.current = false;
-
-        if (isFirstEvent.current) {
-          isFirstEvent.current = false;
-        }
-
-        return;
-      }
-
-      if (isFirstEvent.current) {
-        wasLastEventOverCanvas.current = true;
-        isFirstEvent.current = false;
-      }
-
       if (
         !['Alt', ' '].includes(e.key) ||
         activeTabName !== 'inpainting' ||
@@ -66,8 +52,27 @@ const KeyboardEventManager = () => {
         return;
       }
 
-      if (!wasLastEventOverCanvas.current) {
+      // cursor is NOT over canvas
+      if (!isCursorOnCanvas) {
+        if (!lastEvent.current) {
+          lastEvent.current = e;
+        }
+
+        wasLastEventOverCanvas.current = false;
+        return;
+      }
+
+      // cursor is over canvas
+
+      // if this is the first event
+      if (!lastEvent.current) {
         wasLastEventOverCanvas.current = true;
+        lastEvent.current = e;
+      }
+
+      if (!wasLastEventOverCanvas.current && e.type === 'keyup') {
+        wasLastEventOverCanvas.current = true;
+        lastEvent.current = e;
         return;
       }
 
@@ -83,9 +88,11 @@ const KeyboardEventManager = () => {
           break;
         }
       }
+
+      lastEvent.current = e;
+      wasLastEventOverCanvas.current = true;
     };
 
-    console.log('adding listeners');
     document.addEventListener('keydown', listener);
     document.addEventListener('keyup', listener);
 
diff --git a/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts b/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts
index b80590016e..967a90e098 100644
--- a/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts
+++ b/frontend/src/features/tabs/Inpainting/inpaintingSlice.ts
@@ -1,7 +1,7 @@
 import { createSlice } from '@reduxjs/toolkit';
 import type { PayloadAction } from '@reduxjs/toolkit';
 import { Vector2d } from 'konva/lib/types';
-import { RgbaColor, RgbColor } from 'react-colorful';
+import { RgbaColor } from 'react-colorful';
 import * as InvokeAI from '../../../app/invokeai';
 import _ from 'lodash';
 import { roundDownToMultiple } from '../../../common/util/roundDownToMultiple';
@@ -31,15 +31,15 @@ export type BoundingBoxPreviewType = 'overlay' | 'ants' | 'marchingAnts';
 export interface InpaintingState {
   tool: 'brush' | 'eraser';
   brushSize: number;
-  maskColor: RgbColor;
-  maskOpacity: number;
+  maskColor: RgbaColor;
   cursorPosition: Vector2d | null;
   canvasDimensions: Dimensions;
   boundingBoxDimensions: Dimensions;
   boundingBoxCoordinate: Vector2d;
   isMovingBoundingBox: boolean;
   boundingBoxPreviewFill: RgbaColor;
-  boundingBoxPreviewType: BoundingBoxPreviewType;
+  shouldShowBoundingBoxFill: boolean;
+  isBoundingBoxTransforming: boolean;
   lines: MaskLine[];
   pastLines: MaskLine[][];
   futureLines: MaskLine[][];
@@ -50,18 +50,19 @@ export interface InpaintingState {
   imageToInpaint?: InvokeAI.Image;
   needsRepaint: boolean;
   stageScale: number;
+  isDrawing: boolean;
 }
 
 const initialInpaintingState: InpaintingState = {
   tool: 'brush',
   brushSize: 50,
-  maskColor: { r: 255, g: 90, b: 90 },
-  maskOpacity: 0.5,
+  maskColor: { r: 255, g: 90, b: 90, a: 0.5 },
   canvasDimensions: { width: 0, height: 0 },
   boundingBoxDimensions: { width: 64, height: 64 },
   boundingBoxCoordinate: { x: 0, y: 0 },
-  boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 },
-  boundingBoxPreviewType: 'ants',
+  boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.7 },
+  shouldShowBoundingBoxFill: false,
+  isBoundingBoxTransforming: false,
   cursorPosition: null,
   lines: [],
   pastLines: [],
@@ -72,6 +73,7 @@ const initialInpaintingState: InpaintingState = {
   shouldShowBrushPreview: false,
   isMovingBoundingBox: false,
   needsRepaint: false,
+  isDrawing: false,
   stageScale: 1,
 };
 
@@ -142,12 +144,10 @@ export const inpaintingSlice = createSlice({
     setShouldShowBrushPreview: (state, action: PayloadAction<boolean>) => {
       state.shouldShowBrushPreview = action.payload;
     },
-    setMaskColor: (state, action: PayloadAction<RgbColor>) => {
+    setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
       state.maskColor = action.payload;
     },
-    setMaskOpacity: (state, action: PayloadAction<number>) => {
-      state.maskOpacity = action.payload;
-    },
+    // },
     setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
       state.cursorPosition = action.payload;
     },
@@ -215,34 +215,67 @@ export const inpaintingSlice = createSlice({
       };
     },
     setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
-      const { width: boundingBoxWidth, height: boundingBoxHeight } =
-        action.payload;
-      const { x: boundingBoxX, y: boundingBoxY } = state.boundingBoxCoordinate;
-      const { width: canvasWidth, height: canvasHeight } =
-        state.canvasDimensions;
+      state.boundingBoxDimensions = action.payload;
+      // const { width: boundingBoxWidth, height: boundingBoxHeight } =
+      //   action.payload;
+      // const { x: boundingBoxX, y: boundingBoxY } = state.boundingBoxCoordinate;
+      // const { width: canvasWidth, height: canvasHeight } =
+      //   state.canvasDimensions;
 
-      const overflowX = boundingBoxX + boundingBoxWidth - canvasWidth;
-      const overflowY = boundingBoxY + boundingBoxHeight - canvasHeight;
+      // const roundedCanvasWidth = roundDownToMultiple(canvasWidth, 64);
+      // const roundedCanvasHeight = roundDownToMultiple(canvasHeight, 64);
+      // const roundedBoundingBoxWidth = roundDownToMultiple(boundingBoxWidth, 64);
+      // const roundedBoundingBoxHeight = roundDownToMultiple(
+      //   boundingBoxHeight,
+      //   64
+      // );
 
-      const newBoundingBoxX = roundDownToMultiple(
-        overflowX > 0 ? boundingBoxX - overflowX : boundingBoxX,
-        64
-      );
+      // const overflowX = boundingBoxX + boundingBoxWidth - canvasWidth;
+      // const overflowY = boundingBoxY + boundingBoxHeight - canvasHeight;
 
-      const newBoundingBoxY = roundDownToMultiple(
-        overflowY > 0 ? boundingBoxY - overflowY : boundingBoxY,
-        64
-      );
+      // const newBoundingBoxWidth = _.clamp(
+      //   roundedBoundingBoxWidth,
+      //   64,
+      //   roundedCanvasWidth
+      // );
 
-      state.boundingBoxDimensions = {
-        width: roundDownToMultiple(boundingBoxWidth, 64),
-        height: roundDownToMultiple(boundingBoxHeight, 64),
-      };
+      // const newBoundingBoxHeight = _.clamp(
+      //   roundedBoundingBoxHeight,
+      //   64,
+      //   roundedCanvasHeight
+      // );
 
-      state.boundingBoxCoordinate = {
-        x: newBoundingBoxX,
-        y: newBoundingBoxY,
-      };
+      // const overflowCorrectedX =
+      //   overflowX > 0 ? boundingBoxX - overflowX : boundingBoxX;
+
+      // const overflowCorrectedY =
+      //   overflowY > 0 ? boundingBoxY - overflowY : boundingBoxY;
+
+      // const clampedX = _.clamp(
+      //   overflowCorrectedX,
+      //   64,
+      //   roundedCanvasWidth - newBoundingBoxWidth
+      // );
+
+      // const clampedY = _.clamp(
+      //   overflowCorrectedY,
+      //   64,
+      //   roundedCanvasHeight - newBoundingBoxHeight
+      // );
+
+      // const newBoundingBoxX = roundDownToMultiple(clampedX, 64);
+
+      // const newBoundingBoxY = roundDownToMultiple(clampedY, 64);
+
+      // state.boundingBoxDimensions = {
+      //   width: newBoundingBoxWidth,
+      //   height: newBoundingBoxHeight,
+      // };
+
+      // state.boundingBoxCoordinate = {
+      //   x: newBoundingBoxX,
+      //   y: newBoundingBoxY,
+      // };
     },
     setBoundingBoxCoordinate: (state, action: PayloadAction<Vector2d>) => {
       state.boundingBoxCoordinate = action.payload;
@@ -256,12 +289,6 @@ export const inpaintingSlice = createSlice({
     setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => {
       state.boundingBoxPreviewFill = action.payload;
     },
-    setBoundingBoxPreviewType: (
-      state,
-      action: PayloadAction<BoundingBoxPreviewType>
-    ) => {
-      state.boundingBoxPreviewType = action.payload;
-    },
     setNeedsRepaint: (state, action: PayloadAction<boolean>) => {
       state.needsRepaint = action.payload;
     },
@@ -269,6 +296,15 @@ export const inpaintingSlice = createSlice({
       state.stageScale = action.payload;
       state.needsRepaint = false;
     },
+    setShouldShowBoundingBoxFill: (state, action: PayloadAction<boolean>) => {
+      state.shouldShowBoundingBoxFill = action.payload;
+    },
+    setIsBoundingBoxTransforming: (state, action: PayloadAction<boolean>) => {
+      state.isBoundingBoxTransforming = action.payload;
+    },
+    setIsDrawing: (state, action: PayloadAction<boolean>) => {
+      state.isDrawing = action.payload;
+    },
   },
 });
 
@@ -283,7 +319,6 @@ export const {
   setShouldShowBrushPreview,
   setMaskColor,
   clearMask,
-  setMaskOpacity,
   undo,
   redo,
   setCursorPosition,
@@ -293,11 +328,13 @@ export const {
   setBoundingBoxCoordinate,
   setIsMovingBoundingBox,
   setBoundingBoxPreviewFill,
-  setBoundingBoxPreviewType,
   setNeedsRepaint,
   setStageScale,
   toggleTool,
   toggleIsMovingBoundingBox,
+  setShouldShowBoundingBoxFill,
+  setIsBoundingBoxTransforming,
+  setIsDrawing,
 } = inpaintingSlice.actions;
 
 export default inpaintingSlice.reducer;
diff --git a/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts b/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts
index c58c9bb393..8eba6b922b 100644
--- a/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts
+++ b/frontend/src/features/tabs/Inpainting/inpaintingSliceSelectors.ts
@@ -4,6 +4,18 @@ import { RootState } from '../../../app/store';
 import { OptionsState } from '../../options/optionsSlice';
 import { tabMap } from '../InvokeTabs';
 import { InpaintingState } from './inpaintingSlice';
+import { rgbaColorToRgbString } from './util/colorToString';
+
+export const inpaintingCanvasLinesSelector = createSelector(
+  (state: RootState) => state.inpainting,
+  (inpainting: InpaintingState) => {
+    const { lines, maskColor } = inpainting;
+    return {
+      lines,
+      maskColorString: rgbaColorToRgbString(maskColor),
+    };
+  }
+);
 
 export const inpaintingControlsSelector = createSelector(
   [(state: RootState) => state.inpainting, (state: RootState) => state.options],
@@ -12,7 +24,6 @@ export const inpaintingControlsSelector = createSelector(
       tool,
       brushSize,
       maskColor,
-      maskOpacity,
       shouldInvertMask,
       shouldShowMask,
       shouldShowCheckboardTransparency,
@@ -20,6 +31,7 @@ export const inpaintingControlsSelector = createSelector(
       pastLines,
       futureLines,
       isMovingBoundingBox,
+      shouldShowBoundingBoxFill,
     } = inpainting;
 
     const { activeTab, showDualDisplay } = options;
@@ -28,7 +40,6 @@ export const inpaintingControlsSelector = createSelector(
       tool,
       brushSize,
       maskColor,
-      maskOpacity,
       shouldInvertMask,
       shouldShowMask,
       shouldShowCheckboardTransparency,
@@ -38,6 +49,7 @@ export const inpaintingControlsSelector = createSelector(
       isMovingBoundingBox,
       activeTabName: tabMap[activeTab],
       showDualDisplay,
+      shouldShowBoundingBoxFill,
     };
   },
   {
@@ -58,13 +70,13 @@ export const inpaintingCanvasSelector = createSelector(
       shouldShowMask,
       shouldShowCheckboardTransparency,
       shouldShowBrushPreview,
-      maskOpacity,
       imageToInpaint,
       isMovingBoundingBox,
       boundingBoxDimensions,
       canvasDimensions,
       boundingBoxCoordinate,
       stageScale,
+      shouldShowBoundingBoxFill,
     } = inpainting;
     return {
       tool,
@@ -74,13 +86,13 @@ export const inpaintingCanvasSelector = createSelector(
       shouldShowMask,
       shouldShowCheckboardTransparency,
       shouldShowBrushPreview,
-      maskOpacity,
       imageToInpaint,
       isMovingBoundingBox,
       boundingBoxDimensions,
       canvasDimensions,
       boundingBoxCoordinate,
       stageScale,
+      shouldShowBoundingBoxFill,
     };
   },
   {
diff --git a/frontend/src/features/tabs/Inpainting/util/boundingBoxAnchorBoundDragFunc.ts b/frontend/src/features/tabs/Inpainting/util/boundingBoxAnchorBoundDragFunc.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/src/features/tabs/Inpainting/util/colorToString.ts b/frontend/src/features/tabs/Inpainting/util/colorToString.ts
index 4cdddfa934..8d503f4cd1 100644
--- a/frontend/src/features/tabs/Inpainting/util/colorToString.ts
+++ b/frontend/src/features/tabs/Inpainting/util/colorToString.ts
@@ -5,6 +5,11 @@ export const rgbaColorToString = (color: RgbaColor): string => {
   return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
 
+export const rgbaColorToRgbString = (color: RgbaColor): string => {
+  const { r, g, b } = color;
+  return `rgba(${r}, ${g}, ${b})`;
+};
+
 export const rgbColorToString = (color: RgbColor): string => {
   const { r, g, b } = color;
   return `rgba(${r}, ${g}, ${b})`;
diff --git a/frontend/src/features/tabs/Inpainting/util/constants.ts b/frontend/src/features/tabs/Inpainting/util/constants.ts
index aa2cbf12ce..582f1007d2 100644
--- a/frontend/src/features/tabs/Inpainting/util/constants.ts
+++ b/frontend/src/features/tabs/Inpainting/util/constants.ts
@@ -3,3 +3,8 @@ export const DASH_WIDTH = 4;
 
 // speed of marching ants (lower is faster)
 export const MARCHING_ANTS_SPEED = 30;
+
+// bounding box anchor size
+export const TRANSFORMER_ANCHOR_SIZE = 10;
+
+