scroll the closest element to center (#1670)
Co-authored-by: Sanghyeon Lee <yongdamsh@gmail.com>
This commit is contained in:
parent
0db7ac78c4
commit
fa359034c5
@ -4,7 +4,7 @@ import { getDefaultAppState } from "../appState";
|
|||||||
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getNormalizedZoom, calculateScrollCenter } from "../scene";
|
import { getNormalizedZoom, normalizeScroll } from "../scene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
@ -202,15 +202,22 @@ export const actionZoomToFit = register({
|
|||||||
name: "zoomToFit",
|
name: "zoomToFit",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const nonDeletedElements = elements.filter((element) => !element.isDeleted);
|
const nonDeletedElements = elements.filter((element) => !element.isDeleted);
|
||||||
const scrollCenter = calculateScrollCenter(nonDeletedElements);
|
|
||||||
const commonBounds = getCommonBounds(nonDeletedElements);
|
const commonBounds = getCommonBounds(nonDeletedElements);
|
||||||
const zoom = calculateZoom(commonBounds, appState.zoom, scrollCenter);
|
const [x1, y1, x2, y2] = commonBounds;
|
||||||
|
const centerX = (x1 + x2) / 2;
|
||||||
|
const centerY = (y1 + y2) / 2;
|
||||||
|
const scrollX = normalizeScroll(window.innerWidth / 2 - centerX);
|
||||||
|
const scrollY = normalizeScroll(window.innerHeight / 2 - centerY);
|
||||||
|
const zoom = calculateZoom(commonBounds, appState.zoom, {
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
scrollX: scrollCenter.scrollX,
|
scrollX,
|
||||||
scrollY: scrollCenter.scrollY,
|
scrollY,
|
||||||
zoom,
|
zoom,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
|
@ -847,6 +847,8 @@ class App extends React.Component<any, AppState> {
|
|||||||
remoteElements.filter((element: { isDeleted: boolean }) => {
|
remoteElements.filter((element: { isDeleted: boolean }) => {
|
||||||
return !element.isDeleted;
|
return !element.isDeleted;
|
||||||
}),
|
}),
|
||||||
|
this.state,
|
||||||
|
this.canvas,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -256,7 +256,9 @@ const LayerUI = ({
|
|||||||
<button
|
<button
|
||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState({ ...calculateScrollCenter(elements) });
|
setAppState({
|
||||||
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("buttons.scrollBackToContent")}
|
{t("buttons.scrollBackToContent")}
|
||||||
@ -276,6 +278,7 @@ const LayerUI = ({
|
|||||||
onRoomCreate={onRoomCreate}
|
onRoomCreate={onRoomCreate}
|
||||||
onRoomDestroy={onRoomDestroy}
|
onRoomDestroy={onRoomDestroy}
|
||||||
onLockToggle={onLockToggle}
|
onLockToggle={onLockToggle}
|
||||||
|
canvas={canvas}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="layer-ui__wrapper">
|
<div className="layer-ui__wrapper">
|
||||||
|
@ -27,6 +27,7 @@ type MobileMenuProps = {
|
|||||||
onUsernameChange: (username: string) => void;
|
onUsernameChange: (username: string) => void;
|
||||||
onRoomDestroy: () => void;
|
onRoomDestroy: () => void;
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@ -39,6 +40,7 @@ export const MobileMenu = ({
|
|||||||
onUsernameChange,
|
onUsernameChange,
|
||||||
onRoomDestroy,
|
onRoomDestroy,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
|
canvas,
|
||||||
}: MobileMenuProps) => (
|
}: MobileMenuProps) => (
|
||||||
<>
|
<>
|
||||||
{appState.isLoading && <LoadingMessage />}
|
{appState.isLoading && <LoadingMessage />}
|
||||||
@ -131,7 +133,9 @@ export const MobileMenu = ({
|
|||||||
<button
|
<button
|
||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState({ ...calculateScrollCenter(elements) });
|
setAppState({
|
||||||
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("buttons.scrollBackToContent")}
|
{t("buttons.scrollBackToContent")}
|
||||||
|
@ -271,7 +271,7 @@ export const importFromBackend = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
elements = data.elements || elements;
|
elements = data.elements || elements;
|
||||||
appState = data.appState || appState;
|
appState = { ...appState, ...data.appState };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(t("alerts.importBackendFailed"));
|
window.alert(t("alerts.importBackendFailed"));
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -84,6 +84,5 @@ export const restoreFromLocalStorage = () => {
|
|||||||
// Do nothing because appState is already null
|
// Do nothing because appState is already null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return restore(elements, appState);
|
return restore(elements, appState);
|
||||||
};
|
};
|
||||||
|
@ -121,7 +121,10 @@ export const restore = (
|
|||||||
}, [] as ExcalidrawElement[]);
|
}, [] as ExcalidrawElement[]);
|
||||||
|
|
||||||
if (opts?.scrollToContent && savedState) {
|
if (opts?.scrollToContent && savedState) {
|
||||||
savedState = { ...savedState, ...calculateScrollCenter(elements) };
|
savedState = {
|
||||||
|
...savedState,
|
||||||
|
...calculateScrollCenter(elements, savedState, null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
||||||
import { rotate } from "../math";
|
import { distance2d, rotate } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { Drawable, Op } from "roughjs/bin/core";
|
import { Drawable, Op } from "roughjs/bin/core";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
@ -342,3 +342,27 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
maxY + element.y,
|
maxY + element.y,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getClosestElementBounds = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
from: { x: number; y: number },
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
if (!elements.length) {
|
||||||
|
return [0, 0, 0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
let minDistance = Infinity;
|
||||||
|
let closestElement = elements[0];
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
const [x1, y1, x2, y2] = getElementBounds(element);
|
||||||
|
const distance = distance2d((x1 + x2) / 2, (y1 + y2) / 2, from.x, from.y);
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestElement = element;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return getElementBounds(closestElement);
|
||||||
|
};
|
||||||
|
@ -17,6 +17,7 @@ export {
|
|||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
getArrowPoints,
|
getArrowPoints,
|
||||||
|
getClosestElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -1,12 +1,43 @@
|
|||||||
import { FlooredNumber } from "../types";
|
import { AppState, FlooredNumber } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds, getClosestElementBounds } from "../element";
|
||||||
|
|
||||||
|
import {
|
||||||
|
sceneCoordsToViewportCoords,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
export const normalizeScroll = (pos: number) =>
|
export const normalizeScroll = (pos: number) =>
|
||||||
Math.floor(pos) as FlooredNumber;
|
Math.floor(pos) as FlooredNumber;
|
||||||
|
|
||||||
|
function isOutsideViewPort(
|
||||||
|
appState: AppState,
|
||||||
|
canvas: HTMLCanvasElement | null,
|
||||||
|
cords: Array<number>,
|
||||||
|
) {
|
||||||
|
const [x1, y1, x2, y2] = cords;
|
||||||
|
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||||
|
{ sceneX: x1, sceneY: y1 },
|
||||||
|
appState,
|
||||||
|
canvas,
|
||||||
|
window.devicePixelRatio,
|
||||||
|
);
|
||||||
|
const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords(
|
||||||
|
{ sceneX: x2, sceneY: y2 },
|
||||||
|
appState,
|
||||||
|
canvas,
|
||||||
|
window.devicePixelRatio,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
viewportX2 - viewportX1 > window.innerWidth ||
|
||||||
|
viewportY2 - viewportY1 > window.innerHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const calculateScrollCenter = (
|
export const calculateScrollCenter = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
canvas: HTMLCanvasElement | null,
|
||||||
): { scrollX: FlooredNumber; scrollY: FlooredNumber } => {
|
): { scrollX: FlooredNumber; scrollY: FlooredNumber } => {
|
||||||
if (!elements.length) {
|
if (!elements.length) {
|
||||||
return {
|
return {
|
||||||
@ -14,8 +45,19 @@ export const calculateScrollCenter = (
|
|||||||
scrollY: normalizeScroll(0),
|
scrollY: normalizeScroll(0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const scale = window.devicePixelRatio;
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||||
|
if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
|
||||||
|
[x1, y1, x2, y2] = getClosestElementBounds(
|
||||||
|
elements,
|
||||||
|
viewportCoordsToSceneCoords(
|
||||||
|
{ clientX: appState.scrollX, clientY: appState.scrollY },
|
||||||
|
appState,
|
||||||
|
canvas,
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user