|
|
|
@ -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,70 +259,69 @@ 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 newZoomValue = getNormalizedZoom(
|
|
|
|
|
clamp(roundToStep(adjustedZoomValue, ZOOM_STEP, "floor"), minZoom, maxZoom),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const centerScroll = centerScrollOn({
|
|
|
|
|
scenePoint: { x: centerX, y: centerY },
|
|
|
|
@ -330,18 +329,15 @@ export const zoomToFitBounds = ({
|
|
|
|
|
width: appState.width,
|
|
|
|
|
height: appState.height,
|
|
|
|
|
},
|
|
|
|
|
offsets: canvasOffsets,
|
|
|
|
|
zoom: { value: newZoomValue },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
scrollX = centerScroll.scrollX;
|
|
|
|
|
scrollY = centerScroll.scrollY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 &&
|
|
|
|
|