|
@@ -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 } from "../types";
|
|
|
+import type { AppState, Offsets } from "../types";
|
|
|
import { getShortcutKey, updateActiveTool } from "../utils";
|
|
|
import { register } from "./register";
|
|
|
import { Tooltip } from "../components/Tooltip";
|
|
@@ -38,7 +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";
|
|
|
+import { clamp, roundToStep } from "../../math";
|
|
|
|
|
|
export const actionChangeViewBackgroundColor = register({
|
|
|
name: "changeViewBackgroundColor",
|
|
@@ -259,89 +259,85 @@ const zoomValueToFitBoundsOnViewport = (
|
|
|
const adjustedZoomValue =
|
|
|
smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
|
|
|
|
|
|
- const zoomAdjustedToSteps =
|
|
|
- Math.floor(adjustedZoomValue / ZOOM_STEP) * ZOOM_STEP;
|
|
|
-
|
|
|
- return getNormalizedZoom(Math.min(zoomAdjustedToSteps, 1));
|
|
|
+ return Math.min(adjustedZoomValue, 1);
|
|
|
};
|
|
|
|
|
|
export const zoomToFitBounds = ({
|
|
|
bounds,
|
|
|
appState,
|
|
|
+ canvasOffsets,
|
|
|
fitToViewport = false,
|
|
|
viewportZoomFactor = 1,
|
|
|
+ minZoom = -Infinity,
|
|
|
+ maxZoom = Infinity,
|
|
|
}: {
|
|
|
bounds: SceneBounds;
|
|
|
+ canvasOffsets?: Offsets;
|
|
|
appState: Readonly<AppState>;
|
|
|
/** whether to fit content to viewport (beyond >100%) */
|
|
|
fitToViewport: boolean;
|
|
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
|
|
viewportZoomFactor?: number;
|
|
|
+ minZoom?: number;
|
|
|
+ maxZoom?: number;
|
|
|
}) => {
|
|
|
+ viewportZoomFactor = clamp(viewportZoomFactor, MIN_ZOOM, MAX_ZOOM);
|
|
|
+
|
|
|
const [x1, y1, x2, y2] = bounds;
|
|
|
const centerX = (x1 + x2) / 2;
|
|
|
const centerY = (y1 + y2) / 2;
|
|
|
|
|
|
- let newZoomValue;
|
|
|
- let scrollX;
|
|
|
- let scrollY;
|
|
|
+ const canvasOffsetLeft = canvasOffsets?.left ?? 0;
|
|
|
+ const canvasOffsetTop = canvasOffsets?.top ?? 0;
|
|
|
+ const canvasOffsetRight = canvasOffsets?.right ?? 0;
|
|
|
+ const canvasOffsetBottom = canvasOffsets?.bottom ?? 0;
|
|
|
+
|
|
|
+ const effectiveCanvasWidth =
|
|
|
+ appState.width - canvasOffsetLeft - canvasOffsetRight;
|
|
|
+ const effectiveCanvasHeight =
|
|
|
+ appState.height - canvasOffsetTop - canvasOffsetBottom;
|
|
|
+
|
|
|
+ let adjustedZoomValue;
|
|
|
|
|
|
if (fitToViewport) {
|
|
|
const commonBoundsWidth = x2 - x1;
|
|
|
const commonBoundsHeight = y2 - y1;
|
|
|
|
|
|
- newZoomValue =
|
|
|
+ adjustedZoomValue =
|
|
|
Math.min(
|
|
|
- appState.width / commonBoundsWidth,
|
|
|
- appState.height / commonBoundsHeight,
|
|
|
- ) * clamp(viewportZoomFactor, 0.1, 1);
|
|
|
-
|
|
|
- newZoomValue = getNormalizedZoom(newZoomValue);
|
|
|
-
|
|
|
- let appStateWidth = appState.width;
|
|
|
-
|
|
|
- if (appState.openSidebar) {
|
|
|
- const sidebarDOMElem = document.querySelector(
|
|
|
- ".sidebar",
|
|
|
- ) as HTMLElement | null;
|
|
|
- const sidebarWidth = sidebarDOMElem?.offsetWidth ?? 0;
|
|
|
- const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
|
|
-
|
|
|
- appStateWidth = !isRTL
|
|
|
- ? appState.width - sidebarWidth
|
|
|
- : appState.width + sidebarWidth;
|
|
|
- }
|
|
|
-
|
|
|
- scrollX = (appStateWidth / 2) * (1 / newZoomValue) - centerX;
|
|
|
- scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
|
|
|
+ effectiveCanvasWidth / commonBoundsWidth,
|
|
|
+ effectiveCanvasHeight / commonBoundsHeight,
|
|
|
+ ) * viewportZoomFactor;
|
|
|
} else {
|
|
|
- newZoomValue = zoomValueToFitBoundsOnViewport(
|
|
|
+ adjustedZoomValue = zoomValueToFitBoundsOnViewport(
|
|
|
bounds,
|
|
|
{
|
|
|
- width: appState.width,
|
|
|
- height: appState.height,
|
|
|
+ width: effectiveCanvasWidth,
|
|
|
+ height: effectiveCanvasHeight,
|
|
|
},
|
|
|
viewportZoomFactor,
|
|
|
);
|
|
|
+ }
|
|
|
|
|
|
- const centerScroll = centerScrollOn({
|
|
|
- scenePoint: { x: centerX, y: centerY },
|
|
|
- viewportDimensions: {
|
|
|
- width: appState.width,
|
|
|
- height: appState.height,
|
|
|
- },
|
|
|
- zoom: { value: newZoomValue },
|
|
|
- });
|
|
|
+ const newZoomValue = getNormalizedZoom(
|
|
|
+ clamp(roundToStep(adjustedZoomValue, ZOOM_STEP, "floor"), minZoom, maxZoom),
|
|
|
+ );
|
|
|
|
|
|
- scrollX = centerScroll.scrollX;
|
|
|
- scrollY = centerScroll.scrollY;
|
|
|
- }
|
|
|
+ const centerScroll = centerScrollOn({
|
|
|
+ scenePoint: { x: centerX, y: centerY },
|
|
|
+ viewportDimensions: {
|
|
|
+ width: appState.width,
|
|
|
+ height: appState.height,
|
|
|
+ },
|
|
|
+ offsets: canvasOffsets,
|
|
|
+ zoom: { value: newZoomValue },
|
|
|
+ });
|
|
|
|
|
|
return {
|
|
|
appState: {
|
|
|
...appState,
|
|
|
- scrollX,
|
|
|
- scrollY,
|
|
|
+ scrollX: centerScroll.scrollX,
|
|
|
+ scrollY: centerScroll.scrollY,
|
|
|
zoom: { value: newZoomValue },
|
|
|
},
|
|
|
storeAction: StoreAction.NONE,
|
|
@@ -349,25 +345,34 @@ export const zoomToFitBounds = ({
|
|
|
};
|
|
|
|
|
|
export const zoomToFit = ({
|
|
|
+ canvasOffsets,
|
|
|
targetElements,
|
|
|
appState,
|
|
|
fitToViewport,
|
|
|
viewportZoomFactor,
|
|
|
+ minZoom,
|
|
|
+ maxZoom,
|
|
|
}: {
|
|
|
+ canvasOffsets?: Offsets;
|
|
|
targetElements: readonly ExcalidrawElement[];
|
|
|
appState: Readonly<AppState>;
|
|
|
/** whether to fit content to viewport (beyond >100%) */
|
|
|
fitToViewport: boolean;
|
|
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
|
|
viewportZoomFactor?: number;
|
|
|
+ minZoom?: number;
|
|
|
+ maxZoom?: number;
|
|
|
}) => {
|
|
|
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
|
|
|
|
|
return zoomToFitBounds({
|
|
|
+ canvasOffsets,
|
|
|
bounds: commonBounds,
|
|
|
appState,
|
|
|
fitToViewport,
|
|
|
viewportZoomFactor,
|
|
|
+ minZoom,
|
|
|
+ maxZoom,
|
|
|
});
|
|
|
};
|
|
|
|
|
@@ -388,6 +393,7 @@ export const actionZoomToFitSelectionInViewport = register({
|
|
|
userToFollow: null,
|
|
|
},
|
|
|
fitToViewport: false,
|
|
|
+ canvasOffsets: app.getEditorUIOffsets(),
|
|
|
});
|
|
|
},
|
|
|
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
|
@@ -413,7 +419,7 @@ export const actionZoomToFitSelection = register({
|
|
|
userToFollow: null,
|
|
|
},
|
|
|
fitToViewport: true,
|
|
|
- viewportZoomFactor: 0.7,
|
|
|
+ canvasOffsets: app.getEditorUIOffsets(),
|
|
|
});
|
|
|
},
|
|
|
// NOTE this action should use shift-2 per figma, alas
|
|
@@ -430,7 +436,7 @@ export const actionZoomToFit = register({
|
|
|
icon: zoomAreaIcon,
|
|
|
viewMode: true,
|
|
|
trackEvent: { category: "canvas" },
|
|
|
- perform: (elements, appState) =>
|
|
|
+ perform: (elements, appState, _, app) =>
|
|
|
zoomToFit({
|
|
|
targetElements: elements,
|
|
|
appState: {
|
|
@@ -438,6 +444,7 @@ export const actionZoomToFit = register({
|
|
|
userToFollow: null,
|
|
|
},
|
|
|
fitToViewport: false,
|
|
|
+ canvasOffsets: app.getEditorUIOffsets(),
|
|
|
}),
|
|
|
keyTest: (event) =>
|
|
|
event.code === CODES.ONE &&
|