Browse Source

fix: show bounding box for 3 or more linear point elements (#5554)

* fix: show bounding box for 3+ linear point elements

* refactor

* show bounding box for 3 points as well

* fix dragging bounding box for linear elements

* Increase margin/padding for linear elements

* fix cursor and keep bounding box same but offset resize handles

* introduce slight padding for selection border

* better

* add constant for spacing
Aakansha Doshi 2 years ago
parent
commit
731093f631
4 changed files with 68 additions and 28 deletions
  1. 11 0
      src/components/App.tsx
  2. 5 1
      src/element/collision.ts
  3. 26 5
      src/element/transformHandles.ts
  4. 26 22
      src/renderer/renderScene.ts

+ 11 - 0
src/components/App.tsx

@@ -261,6 +261,7 @@ import {
   isPointHittingLinkIcon,
   isPointHittingLinkIcon,
   isLocalLink,
   isLocalLink,
 } from "../element/Hyperlink";
 } from "../element/Hyperlink";
+import { shouldShowBoundingBox } from "../element/transformHandles";
 
 
 const deviceContextInitialValue = {
 const deviceContextInitialValue = {
   isSmScreen: false,
   isSmScreen: false,
@@ -3046,6 +3047,16 @@ class App extends React.Component<AppProps, AppState> {
         } else {
         } else {
           setCursor(this.canvas, CURSOR_TYPE.MOVE);
           setCursor(this.canvas, CURSOR_TYPE.MOVE);
         }
         }
+      } else if (
+        shouldShowBoundingBox([element]) &&
+        isHittingElementBoundingBoxWithoutHittingElement(
+          element,
+          this.state,
+          scenePointerX,
+          scenePointerY,
+        )
+      ) {
+        setCursor(this.canvas, CURSOR_TYPE.MOVE);
       }
       }
 
 
       if (
       if (

+ 5 - 1
src/element/collision.ts

@@ -35,6 +35,7 @@ import { getShapeForElement } from "../renderer/renderElement";
 import { hasBoundTextElement, isImageElement } from "./typeChecks";
 import { hasBoundTextElement, isImageElement } from "./typeChecks";
 import { isTextElement } from ".";
 import { isTextElement } from ".";
 import { isTransparent } from "../utils";
 import { isTransparent } from "../utils";
+import { shouldShowBoundingBox } from "./transformHandles";
 
 
 const isElementDraggableFromInside = (
 const isElementDraggableFromInside = (
   element: NonDeletedExcalidrawElement,
   element: NonDeletedExcalidrawElement,
@@ -64,7 +65,10 @@ export const hitTest = (
   const threshold = 10 / appState.zoom.value;
   const threshold = 10 / appState.zoom.value;
   const point: Point = [x, y];
   const point: Point = [x, y];
 
 
-  if (isElementSelected(appState, element) && !appState.selectedLinearElement) {
+  if (
+    isElementSelected(appState, element) &&
+    shouldShowBoundingBox([element])
+  ) {
     return isPointHittingElementBoundingBox(element, point, threshold);
     return isPointHittingElementBoundingBox(element, point, threshold);
   }
   }
 
 

+ 26 - 5
src/element/transformHandles.ts

@@ -1,10 +1,15 @@
-import { ExcalidrawElement, PointerType } from "./types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+  PointerType,
+} from "./types";
 
 
 import { getElementAbsoluteCoords, Bounds } from "./bounds";
 import { getElementAbsoluteCoords, Bounds } from "./bounds";
 import { rotate } from "../math";
 import { rotate } from "../math";
 import { Zoom } from "../types";
 import { Zoom } from "../types";
 import { isTextElement } from ".";
 import { isTextElement } from ".";
 import { isLinearElement } from "./typeChecks";
 import { isLinearElement } from "./typeChecks";
+import { DEFAULT_SPACING } from "../renderer/renderScene";
 
 
 export type TransformHandleDirection =
 export type TransformHandleDirection =
   | "n"
   | "n"
@@ -81,6 +86,7 @@ export const getTransformHandlesFromCoords = (
   zoom: Zoom,
   zoom: Zoom,
   pointerType: PointerType,
   pointerType: PointerType,
   omitSides: { [T in TransformHandleType]?: boolean } = {},
   omitSides: { [T in TransformHandleType]?: boolean } = {},
+  margin = 4,
 ): TransformHandles => {
 ): TransformHandles => {
   const size = transformHandleSizes[pointerType];
   const size = transformHandleSizes[pointerType];
   const handleWidth = size / zoom.value;
   const handleWidth = size / zoom.value;
@@ -93,9 +99,7 @@ export const getTransformHandlesFromCoords = (
   const height = y2 - y1;
   const height = y2 - y1;
   const cx = (x1 + x2) / 2;
   const cx = (x1 + x2) / 2;
   const cy = (y1 + y2) / 2;
   const cy = (y1 + y2) / 2;
-
-  const dashedLineMargin = 4 / zoom.value;
-
+  const dashedLineMargin = margin / zoom.value;
   const centeringOffset = (size - 8) / (2 * zoom.value);
   const centeringOffset = (size - 8) / (2 * zoom.value);
 
 
   const transformHandles: TransformHandles = {
   const transformHandles: TransformHandles = {
@@ -248,12 +252,29 @@ export const getTransformHandles = (
   } else if (isTextElement(element)) {
   } else if (isTextElement(element)) {
     omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
     omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
   }
   }
-
+  const dashedLineMargin = isLinearElement(element)
+    ? DEFAULT_SPACING * 3
+    : DEFAULT_SPACING;
   return getTransformHandlesFromCoords(
   return getTransformHandlesFromCoords(
     getElementAbsoluteCoords(element),
     getElementAbsoluteCoords(element),
     element.angle,
     element.angle,
     zoom,
     zoom,
     pointerType,
     pointerType,
     omitSides,
     omitSides,
+    dashedLineMargin,
   );
   );
 };
 };
+
+export const shouldShowBoundingBox = (
+  elements: NonDeletedExcalidrawElement[],
+) => {
+  if (elements.length > 1) {
+    return true;
+  }
+  const element = elements[0];
+  if (!isLinearElement(element)) {
+    return true;
+  }
+
+  return element.points.length > 2;
+};

+ 26 - 22
src/renderer/renderScene.ts

@@ -44,6 +44,7 @@ import {
   isBindingEnabled,
   isBindingEnabled,
 } from "../element/binding";
 } from "../element/binding";
 import {
 import {
+  shouldShowBoundingBox,
   TransformHandles,
   TransformHandles,
   TransformHandleType,
   TransformHandleType,
 } from "../element/transformHandles";
 } from "../element/transformHandles";
@@ -61,6 +62,7 @@ import {
 import { isLinearElement } from "../element/typeChecks";
 import { isLinearElement } from "../element/typeChecks";
 
 
 const hasEmojiSupport = supportsEmoji();
 const hasEmojiSupport = supportsEmoji();
+export const DEFAULT_SPACING = 4;
 
 
 const strokeRectWithRotation = (
 const strokeRectWithRotation = (
   context: CanvasRenderingContext2D,
   context: CanvasRenderingContext2D,
@@ -219,6 +221,7 @@ const renderLinearElementPointHighlight = (
 
 
   context.restore();
   context.restore();
 };
 };
+
 export const _renderScene = (
 export const _renderScene = (
   elements: readonly NonDeletedExcalidrawElement[],
   elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   appState: AppState,
@@ -346,7 +349,6 @@ export const _renderScene = (
   ) {
   ) {
     renderLinearElementPointHighlight(context, appState, renderConfig);
     renderLinearElementPointHighlight(context, appState, renderConfig);
   }
   }
-
   // Paint selected elements
   // Paint selected elements
   if (
   if (
     renderSelection &&
     renderSelection &&
@@ -354,6 +356,8 @@ export const _renderScene = (
     !appState.editingLinearElement
     !appState.editingLinearElement
   ) {
   ) {
     const locallySelectedElements = getSelectedElements(elements, appState);
     const locallySelectedElements = getSelectedElements(elements, appState);
+    const showBoundingBox = shouldShowBoundingBox(locallySelectedElements);
+
     const locallySelectedIds = locallySelectedElements.map(
     const locallySelectedIds = locallySelectedElements.map(
       (element) => element.id,
       (element) => element.id,
     );
     );
@@ -373,9 +377,8 @@ export const _renderScene = (
         renderConfig,
         renderConfig,
         locallySelectedElements[0] as ExcalidrawLinearElement,
         locallySelectedElements[0] as ExcalidrawLinearElement,
       );
       );
-      // render bounding box
-      // (unless dragging a single linear element)
-    } else if (!appState.draggingElement || !isSingleLinearElementSelected) {
+    }
+    if (showBoundingBox) {
       const selections = elements.reduce((acc, element) => {
       const selections = elements.reduce((acc, element) => {
         const selectionColors = [];
         const selectionColors = [];
         // local user
         // local user
@@ -434,12 +437,18 @@ export const _renderScene = (
         addSelectionForGroupId(appState.editingGroupId);
         addSelectionForGroupId(appState.editingGroupId);
       }
       }
       selections.forEach((selection) =>
       selections.forEach((selection) =>
-        renderSelectionBorder(context, renderConfig, selection),
+        renderSelectionBorder(
+          context,
+          renderConfig,
+          selection,
+          isSingleLinearElementSelected ? DEFAULT_SPACING * 2 : DEFAULT_SPACING,
+        ),
       );
       );
     }
     }
     // Paint resize transformHandles
     // Paint resize transformHandles
     context.save();
     context.save();
     context.translate(renderConfig.scrollX, renderConfig.scrollY);
     context.translate(renderConfig.scrollX, renderConfig.scrollY);
+
     if (locallySelectedElements.length === 1) {
     if (locallySelectedElements.length === 1) {
       context.fillStyle = oc.white;
       context.fillStyle = oc.white;
       const transformHandles = getTransformHandles(
       const transformHandles = getTransformHandles(
@@ -447,10 +456,7 @@ export const _renderScene = (
         renderConfig.zoom,
         renderConfig.zoom,
         "mouse", // when we render we don't know which pointer type so use mouse
         "mouse", // when we render we don't know which pointer type so use mouse
       );
       );
-      if (
-        !appState.viewModeEnabled &&
-        !isLinearElement(locallySelectedElements[0])
-      ) {
+      if (!appState.viewModeEnabled && showBoundingBox) {
         renderTransformHandles(
         renderTransformHandles(
           context,
           context,
           renderConfig,
           renderConfig,
@@ -714,24 +720,21 @@ const renderTransformHandles = (
   Object.keys(transformHandles).forEach((key) => {
   Object.keys(transformHandles).forEach((key) => {
     const transformHandle = transformHandles[key as TransformHandleType];
     const transformHandle = transformHandles[key as TransformHandleType];
     if (transformHandle !== undefined) {
     if (transformHandle !== undefined) {
+      const [x, y, width, height] = transformHandle;
+
       context.save();
       context.save();
       context.lineWidth = 1 / renderConfig.zoom.value;
       context.lineWidth = 1 / renderConfig.zoom.value;
       if (key === "rotation") {
       if (key === "rotation") {
-        fillCircle(
-          context,
-          transformHandle[0] + transformHandle[2] / 2,
-          transformHandle[1] + transformHandle[3] / 2,
-          transformHandle[2] / 2,
-        );
+        fillCircle(context, x + width / 2, y + height / 2, width / 2);
       } else {
       } else {
         strokeRectWithRotation(
         strokeRectWithRotation(
           context,
           context,
-          transformHandle[0],
-          transformHandle[1],
-          transformHandle[2],
-          transformHandle[3],
-          transformHandle[0] + transformHandle[2] / 2,
-          transformHandle[1] + transformHandle[3] / 2,
+          x,
+          y,
+          width,
+          height,
+          x + width / 2,
+          y + height / 2,
           angle,
           angle,
           true, // fill before stroke
           true, // fill before stroke
         );
         );
@@ -752,13 +755,14 @@ const renderSelectionBorder = (
     elementY2: number;
     elementY2: number;
     selectionColors: string[];
     selectionColors: string[];
   },
   },
+  padding = 4,
 ) => {
 ) => {
   const { angle, elementX1, elementY1, elementX2, elementY2, selectionColors } =
   const { angle, elementX1, elementY1, elementX2, elementY2, selectionColors } =
     elementProperties;
     elementProperties;
   const elementWidth = elementX2 - elementX1;
   const elementWidth = elementX2 - elementX1;
   const elementHeight = elementY2 - elementY1;
   const elementHeight = elementY2 - elementY1;
 
 
-  const dashedLinePadding = 4 / renderConfig.zoom.value;
+  const dashedLinePadding = padding / renderConfig.zoom.value;
   const dashWidth = 8 / renderConfig.zoom.value;
   const dashWidth = 8 / renderConfig.zoom.value;
   const spaceWidth = 4 / renderConfig.zoom.value;
   const spaceWidth = 4 / renderConfig.zoom.value;