Browse Source

feat: improve zoom-to-content when creating flowchart (#8368)

David Luzar 1 year ago
parent
commit
4320a3cf41

+ 22 - 17
packages/excalidraw/actions/actionCanvas.tsx

@@ -24,7 +24,7 @@ import { CODES, KEYS } from "../keys";
 import { getNormalizedZoom } from "../scene";
 import { centerScrollOn } from "../scene/scroll";
 import { getStateForZoom } from "../scene/zoom";
-import type { AppState, NormalizedZoomValue } from "../types";
+import type { AppState } from "../types";
 import { getShortcutKey, updateActiveTool } from "../utils";
 import { register } from "./register";
 import { Tooltip } from "../components/Tooltip";
@@ -38,6 +38,7 @@ import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
 import type { SceneBounds } from "../element/bounds";
 import { setCursor } from "../cursor";
 import { StoreAction } from "../store";
+import { clamp } from "../math";
 
 export const actionChangeViewBackgroundColor = register({
   name: "changeViewBackgroundColor",
@@ -244,6 +245,7 @@ export const actionResetZoom = register({
 const zoomValueToFitBoundsOnViewport = (
   bounds: SceneBounds,
   viewportDimensions: { width: number; height: number },
+  viewportZoomFactor: number = 1, // default to 1 if not provided
 ) => {
   const [x1, y1, x2, y2] = bounds;
   const commonBoundsWidth = x2 - x1;
@@ -251,20 +253,21 @@ const zoomValueToFitBoundsOnViewport = (
   const commonBoundsHeight = y2 - y1;
   const zoomValueForHeight = viewportDimensions.height / commonBoundsHeight;
   const smallestZoomValue = Math.min(zoomValueForWidth, zoomValueForHeight);
+
+  const adjustedZoomValue =
+    smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
+
   const zoomAdjustedToSteps =
-    Math.floor(smallestZoomValue / ZOOM_STEP) * ZOOM_STEP;
-  const clampedZoomValueToFitElements = Math.min(
-    Math.max(zoomAdjustedToSteps, MIN_ZOOM),
-    1,
-  );
-  return clampedZoomValueToFitElements as NormalizedZoomValue;
+    Math.floor(adjustedZoomValue / ZOOM_STEP) * ZOOM_STEP;
+
+  return getNormalizedZoom(Math.min(zoomAdjustedToSteps, 1));
 };
 
 export const zoomToFitBounds = ({
   bounds,
   appState,
   fitToViewport = false,
-  viewportZoomFactor = 0.7,
+  viewportZoomFactor = 1,
 }: {
   bounds: SceneBounds;
   appState: Readonly<AppState>;
@@ -289,13 +292,10 @@ export const zoomToFitBounds = ({
       Math.min(
         appState.width / commonBoundsWidth,
         appState.height / commonBoundsHeight,
-      ) * Math.min(1, Math.max(viewportZoomFactor, 0.1));
+      ) * clamp(viewportZoomFactor, 0.1, 1);
 
     // Apply clamping to newZoomValue to be between 10% and 3000%
-    newZoomValue = Math.min(
-      Math.max(newZoomValue, MIN_ZOOM),
-      MAX_ZOOM,
-    ) as NormalizedZoomValue;
+    newZoomValue = getNormalizedZoom(newZoomValue);
 
     let appStateWidth = appState.width;
 
@@ -314,10 +314,14 @@ export const zoomToFitBounds = ({
     scrollX = (appStateWidth / 2) * (1 / newZoomValue) - centerX;
     scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
   } else {
-    newZoomValue = zoomValueToFitBoundsOnViewport(bounds, {
-      width: appState.width,
-      height: appState.height,
-    });
+    newZoomValue = zoomValueToFitBoundsOnViewport(
+      bounds,
+      {
+        width: appState.width,
+        height: appState.height,
+      },
+      viewportZoomFactor,
+    );
 
     const centerScroll = centerScrollOn({
       scenePoint: { x: centerX, y: centerY },
@@ -408,6 +412,7 @@ export const actionZoomToFitSelection = register({
         userToFollow: null,
       },
       fitToViewport: true,
+      viewportZoomFactor: 0.7,
     });
   },
   // NOTE this action should use shift-2 per figma, alas

+ 67 - 3
packages/excalidraw/components/App.tsx

@@ -3595,7 +3595,7 @@ class App extends React.Component<AppProps, AppState> {
       | {
           fitToContent?: boolean;
           fitToViewport?: never;
-          viewportZoomFactor?: never;
+          viewportZoomFactor?: number;
           animate?: boolean;
           duration?: number;
         }
@@ -3860,6 +3860,43 @@ class App extends React.Component<AppProps, AppState> {
     },
   );
 
+  private getEditorUIOffsets = (): {
+    top: number;
+    right: number;
+    bottom: number;
+    left: number;
+  } => {
+    const toolbarBottom =
+      this.excalidrawContainerRef?.current
+        ?.querySelector(".App-toolbar")
+        ?.getBoundingClientRect()?.bottom ?? 0;
+    const sidebarWidth = Math.max(
+      this.excalidrawContainerRef?.current
+        ?.querySelector(".default-sidebar")
+        ?.getBoundingClientRect()?.width ?? 0,
+    );
+    const propertiesPanelWidth = Math.max(
+      this.excalidrawContainerRef?.current
+        ?.querySelector(".App-menu__left")
+        ?.getBoundingClientRect()?.width ?? 0,
+      0,
+    );
+
+    return getLanguage().rtl
+      ? {
+          top: toolbarBottom,
+          right: propertiesPanelWidth,
+          bottom: 0,
+          left: sidebarWidth,
+        }
+      : {
+          top: toolbarBottom,
+          right: sidebarWidth,
+          bottom: 0,
+          left: propertiesPanelWidth,
+        };
+  };
+
   // Input handling
   private onKeyDown = withBatchedUpdates(
     (event: React.KeyboardEvent | KeyboardEvent) => {
@@ -3920,6 +3957,31 @@ class App extends React.Component<AppProps, AppState> {
             );
           }
 
+          if (
+            this.flowChartCreator.pendingNodes?.length &&
+            !isElementCompletelyInViewport(
+              this.flowChartCreator.pendingNodes,
+              this.canvas.width / window.devicePixelRatio,
+              this.canvas.height / window.devicePixelRatio,
+              {
+                offsetLeft: this.state.offsetLeft,
+                offsetTop: this.state.offsetTop,
+                scrollX: this.state.scrollX,
+                scrollY: this.state.scrollY,
+                zoom: this.state.zoom,
+              },
+              this.scene.getNonDeletedElementsMap(),
+              this.getEditorUIOffsets(),
+            )
+          ) {
+            this.scrollToContent(this.flowChartCreator.pendingNodes, {
+              animate: true,
+              duration: 300,
+              fitToContent: true,
+              viewportZoomFactor: 0.8,
+            });
+          }
+
           return;
         }
 
@@ -3955,7 +4017,7 @@ class App extends React.Component<AppProps, AppState> {
               if (
                 nextNode &&
                 !isElementCompletelyInViewport(
-                  nextNode,
+                  [nextNode],
                   this.canvas.width / window.devicePixelRatio,
                   this.canvas.height / window.devicePixelRatio,
                   {
@@ -3966,6 +4028,7 @@ class App extends React.Component<AppProps, AppState> {
                     zoom: this.state.zoom,
                   },
                   this.scene.getNonDeletedElementsMap(),
+                  this.getEditorUIOffsets(),
                 )
               ) {
                 this.scrollToContent(nextNode, {
@@ -4373,7 +4436,7 @@ class App extends React.Component<AppProps, AppState> {
 
           if (
             !isElementCompletelyInViewport(
-              firstNode,
+              [firstNode],
               this.canvas.width / window.devicePixelRatio,
               this.canvas.height / window.devicePixelRatio,
               {
@@ -4384,6 +4447,7 @@ class App extends React.Component<AppProps, AppState> {
                 zoom: this.state.zoom,
               },
               this.scene.getNonDeletedElementsMap(),
+              this.getEditorUIOffsets(),
             )
           ) {
             this.scrollToContent(firstNode, {

+ 5 - 3
packages/excalidraw/element/bounds.ts

@@ -738,6 +738,7 @@ export const getElementBounds = (
 
 export const getCommonBounds = (
   elements: readonly ExcalidrawElement[],
+  elementsMap?: ElementsMap,
 ): Bounds => {
   if (!elements.length) {
     return [0, 0, 0, 0];
@@ -748,10 +749,11 @@ export const getCommonBounds = (
   let minY = Infinity;
   let maxY = -Infinity;
 
-  const elementsMap = arrayToMap(elements);
-
   elements.forEach((element) => {
-    const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
+    const [x1, y1, x2, y2] = getElementBounds(
+      element,
+      elementsMap || arrayToMap(elements),
+    );
     minX = Math.min(minX, x1);
     minY = Math.min(minY, y1);
     maxX = Math.max(maxX, x2);

+ 13 - 7
packages/excalidraw/element/sizeHelpers.ts

@@ -3,7 +3,7 @@ import { mutateElement } from "./mutateElement";
 import { isFreeDrawElement, isLinearElement } from "./typeChecks";
 import { SHIFT_LOCKING_ANGLE } from "../constants";
 import type { AppState, Zoom } from "../types";
-import { getElementBounds } from "./bounds";
+import { getCommonBounds, getElementBounds } from "./bounds";
 import { viewportCoordsToSceneCoords } from "../utils";
 
 // TODO:  remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
@@ -56,7 +56,7 @@ export const isElementInViewport = (
 };
 
 export const isElementCompletelyInViewport = (
-  element: ExcalidrawElement,
+  elements: ExcalidrawElement[],
   width: number,
   height: number,
   viewTransformations: {
@@ -67,19 +67,25 @@ export const isElementCompletelyInViewport = (
     scrollY: number;
   },
   elementsMap: ElementsMap,
+  padding?: Partial<{
+    top: number;
+    right: number;
+    bottom: number;
+    left: number;
+  }>,
 ) => {
-  const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
+  const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
   const topLeftSceneCoords = viewportCoordsToSceneCoords(
     {
-      clientX: viewTransformations.offsetLeft,
-      clientY: viewTransformations.offsetTop,
+      clientX: viewTransformations.offsetLeft + (padding?.left || 0),
+      clientY: viewTransformations.offsetTop + (padding?.top || 0),
     },
     viewTransformations,
   );
   const bottomRightSceneCoords = viewportCoordsToSceneCoords(
     {
-      clientX: viewTransformations.offsetLeft + width,
-      clientY: viewTransformations.offsetTop + height,
+      clientX: viewTransformations.offsetLeft + width - (padding?.right || 0),
+      clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0),
     },
     viewTransformations,
   );