mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: improve canvas search scroll behavior further (#8491)
This commit is contained in:
parent
b46ca0192b
commit
fd39712ba6
8 changed files with 165 additions and 99 deletions
|
@ -24,7 +24,7 @@ import { CODES, KEYS } from "../keys";
|
||||||
import { getNormalizedZoom } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import type { AppState } from "../types";
|
import type { AppState, Offsets } from "../types";
|
||||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
|
@ -38,7 +38,7 @@ import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
||||||
import type { SceneBounds } from "../element/bounds";
|
import type { SceneBounds } from "../element/bounds";
|
||||||
import { setCursor } from "../cursor";
|
import { setCursor } from "../cursor";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { clamp } from "../../math";
|
import { clamp, roundToStep } from "../../math";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
|
@ -259,89 +259,85 @@ const zoomValueToFitBoundsOnViewport = (
|
||||||
const adjustedZoomValue =
|
const adjustedZoomValue =
|
||||||
smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
|
smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
|
||||||
|
|
||||||
const zoomAdjustedToSteps =
|
return Math.min(adjustedZoomValue, 1);
|
||||||
Math.floor(adjustedZoomValue / ZOOM_STEP) * ZOOM_STEP;
|
|
||||||
|
|
||||||
return getNormalizedZoom(Math.min(zoomAdjustedToSteps, 1));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomToFitBounds = ({
|
export const zoomToFitBounds = ({
|
||||||
bounds,
|
bounds,
|
||||||
appState,
|
appState,
|
||||||
|
canvasOffsets,
|
||||||
fitToViewport = false,
|
fitToViewport = false,
|
||||||
viewportZoomFactor = 1,
|
viewportZoomFactor = 1,
|
||||||
|
minZoom = -Infinity,
|
||||||
|
maxZoom = Infinity,
|
||||||
}: {
|
}: {
|
||||||
bounds: SceneBounds;
|
bounds: SceneBounds;
|
||||||
|
canvasOffsets?: Offsets;
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
fitToViewport: boolean;
|
fitToViewport: boolean;
|
||||||
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
viewportZoomFactor = clamp(viewportZoomFactor, MIN_ZOOM, MAX_ZOOM);
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = bounds;
|
const [x1, y1, x2, y2] = bounds;
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
|
||||||
let newZoomValue;
|
const canvasOffsetLeft = canvasOffsets?.left ?? 0;
|
||||||
let scrollX;
|
const canvasOffsetTop = canvasOffsets?.top ?? 0;
|
||||||
let scrollY;
|
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) {
|
if (fitToViewport) {
|
||||||
const commonBoundsWidth = x2 - x1;
|
const commonBoundsWidth = x2 - x1;
|
||||||
const commonBoundsHeight = y2 - y1;
|
const commonBoundsHeight = y2 - y1;
|
||||||
|
|
||||||
newZoomValue =
|
adjustedZoomValue =
|
||||||
Math.min(
|
Math.min(
|
||||||
appState.width / commonBoundsWidth,
|
effectiveCanvasWidth / commonBoundsWidth,
|
||||||
appState.height / commonBoundsHeight,
|
effectiveCanvasHeight / commonBoundsHeight,
|
||||||
) * clamp(viewportZoomFactor, 0.1, 1);
|
) * viewportZoomFactor;
|
||||||
|
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
newZoomValue = zoomValueToFitBoundsOnViewport(
|
adjustedZoomValue = zoomValueToFitBoundsOnViewport(
|
||||||
bounds,
|
bounds,
|
||||||
{
|
{
|
||||||
width: appState.width,
|
width: effectiveCanvasWidth,
|
||||||
height: appState.height,
|
height: effectiveCanvasHeight,
|
||||||
},
|
},
|
||||||
viewportZoomFactor,
|
viewportZoomFactor,
|
||||||
);
|
);
|
||||||
|
|
||||||
const centerScroll = centerScrollOn({
|
|
||||||
scenePoint: { x: centerX, y: centerY },
|
|
||||||
viewportDimensions: {
|
|
||||||
width: appState.width,
|
|
||||||
height: appState.height,
|
|
||||||
},
|
|
||||||
zoom: { value: newZoomValue },
|
|
||||||
});
|
|
||||||
|
|
||||||
scrollX = centerScroll.scrollX;
|
|
||||||
scrollY = centerScroll.scrollY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newZoomValue = getNormalizedZoom(
|
||||||
|
clamp(roundToStep(adjustedZoomValue, ZOOM_STEP, "floor"), minZoom, maxZoom),
|
||||||
|
);
|
||||||
|
|
||||||
|
const centerScroll = centerScrollOn({
|
||||||
|
scenePoint: { x: centerX, y: centerY },
|
||||||
|
viewportDimensions: {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
},
|
||||||
|
offsets: canvasOffsets,
|
||||||
|
zoom: { value: newZoomValue },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
scrollX,
|
scrollX: centerScroll.scrollX,
|
||||||
scrollY,
|
scrollY: centerScroll.scrollY,
|
||||||
zoom: { value: newZoomValue },
|
zoom: { value: newZoomValue },
|
||||||
},
|
},
|
||||||
storeAction: StoreAction.NONE,
|
storeAction: StoreAction.NONE,
|
||||||
|
@ -349,25 +345,34 @@ export const zoomToFitBounds = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomToFit = ({
|
export const zoomToFit = ({
|
||||||
|
canvasOffsets,
|
||||||
targetElements,
|
targetElements,
|
||||||
appState,
|
appState,
|
||||||
fitToViewport,
|
fitToViewport,
|
||||||
viewportZoomFactor,
|
viewportZoomFactor,
|
||||||
|
minZoom,
|
||||||
|
maxZoom,
|
||||||
}: {
|
}: {
|
||||||
|
canvasOffsets?: Offsets;
|
||||||
targetElements: readonly ExcalidrawElement[];
|
targetElements: readonly ExcalidrawElement[];
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
fitToViewport: boolean;
|
fitToViewport: boolean;
|
||||||
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
||||||
|
|
||||||
return zoomToFitBounds({
|
return zoomToFitBounds({
|
||||||
|
canvasOffsets,
|
||||||
bounds: commonBounds,
|
bounds: commonBounds,
|
||||||
appState,
|
appState,
|
||||||
fitToViewport,
|
fitToViewport,
|
||||||
viewportZoomFactor,
|
viewportZoomFactor,
|
||||||
|
minZoom,
|
||||||
|
maxZoom,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -388,6 +393,7 @@ export const actionZoomToFitSelectionInViewport = register({
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
fitToViewport: false,
|
fitToViewport: false,
|
||||||
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
||||||
|
@ -413,7 +419,7 @@ export const actionZoomToFitSelection = register({
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
fitToViewport: true,
|
fitToViewport: true,
|
||||||
viewportZoomFactor: 0.7,
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// NOTE this action should use shift-2 per figma, alas
|
// NOTE this action should use shift-2 per figma, alas
|
||||||
|
@ -430,7 +436,7 @@ export const actionZoomToFit = register({
|
||||||
icon: zoomAreaIcon,
|
icon: zoomAreaIcon,
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) =>
|
perform: (elements, appState, _, app) =>
|
||||||
zoomToFit({
|
zoomToFit({
|
||||||
targetElements: elements,
|
targetElements: elements,
|
||||||
appState: {
|
appState: {
|
||||||
|
@ -438,6 +444,7 @@ export const actionZoomToFit = register({
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
fitToViewport: false,
|
fitToViewport: false,
|
||||||
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
}),
|
}),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.ONE &&
|
event.code === CODES.ONE &&
|
||||||
|
|
|
@ -259,6 +259,7 @@ import type {
|
||||||
ElementsPendingErasure,
|
ElementsPendingErasure,
|
||||||
GenerateDiagramToCode,
|
GenerateDiagramToCode,
|
||||||
NullableGridSize,
|
NullableGridSize,
|
||||||
|
Offsets,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
|
@ -3232,6 +3233,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (opts.fitToContent) {
|
if (opts.fitToContent) {
|
||||||
this.scrollToContent(newElements, {
|
this.scrollToContent(newElements, {
|
||||||
fitToContent: true,
|
fitToContent: true,
|
||||||
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -3544,7 +3546,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
target:
|
target:
|
||||||
| ExcalidrawElement
|
| ExcalidrawElement
|
||||||
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
||||||
opts?:
|
opts?: (
|
||||||
| {
|
| {
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
fitToViewport?: never;
|
fitToViewport?: never;
|
||||||
|
@ -3561,7 +3563,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
},
|
}
|
||||||
|
) & {
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
canvasOffsets?: Offsets;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
this.cancelInProgressAnimation?.();
|
this.cancelInProgressAnimation?.();
|
||||||
|
|
||||||
|
@ -3574,10 +3581,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
if (opts?.fitToContent || opts?.fitToViewport) {
|
if (opts?.fitToContent || opts?.fitToViewport) {
|
||||||
const { appState } = zoomToFit({
|
const { appState } = zoomToFit({
|
||||||
|
canvasOffsets: opts.canvasOffsets,
|
||||||
targetElements,
|
targetElements,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
fitToViewport: !!opts?.fitToViewport,
|
fitToViewport: !!opts?.fitToViewport,
|
||||||
viewportZoomFactor: opts?.viewportZoomFactor,
|
viewportZoomFactor: opts?.viewportZoomFactor,
|
||||||
|
minZoom: opts?.minZoom,
|
||||||
|
maxZoom: opts?.maxZoom,
|
||||||
});
|
});
|
||||||
zoom = appState.zoom;
|
zoom = appState.zoom;
|
||||||
scrollX = appState.scrollX;
|
scrollX = appState.scrollX;
|
||||||
|
@ -3805,40 +3815,42 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
public getEditorUIOffsets = (): {
|
public getEditorUIOffsets = (): Offsets => {
|
||||||
top: number;
|
|
||||||
right: number;
|
|
||||||
bottom: number;
|
|
||||||
left: number;
|
|
||||||
} => {
|
|
||||||
const toolbarBottom =
|
const toolbarBottom =
|
||||||
this.excalidrawContainerRef?.current
|
this.excalidrawContainerRef?.current
|
||||||
?.querySelector(".App-toolbar")
|
?.querySelector(".App-toolbar")
|
||||||
?.getBoundingClientRect()?.bottom ?? 0;
|
?.getBoundingClientRect()?.bottom ?? 0;
|
||||||
const sidebarWidth = Math.max(
|
const sidebarRect = this.excalidrawContainerRef?.current
|
||||||
this.excalidrawContainerRef?.current
|
?.querySelector(".sidebar")
|
||||||
?.querySelector(".default-sidebar")
|
?.getBoundingClientRect();
|
||||||
?.getBoundingClientRect()?.width ?? 0,
|
const propertiesPanelRect = this.excalidrawContainerRef?.current
|
||||||
);
|
?.querySelector(".App-menu__left")
|
||||||
const propertiesPanelWidth = Math.max(
|
?.getBoundingClientRect();
|
||||||
this.excalidrawContainerRef?.current
|
|
||||||
?.querySelector(".App-menu__left")
|
const PADDING = 16;
|
||||||
?.getBoundingClientRect()?.width ?? 0,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return getLanguage().rtl
|
return getLanguage().rtl
|
||||||
? {
|
? {
|
||||||
top: toolbarBottom,
|
top: toolbarBottom + PADDING,
|
||||||
right: propertiesPanelWidth,
|
right:
|
||||||
bottom: 0,
|
Math.max(
|
||||||
left: sidebarWidth,
|
this.state.width -
|
||||||
|
(propertiesPanelRect?.left ?? this.state.width),
|
||||||
|
0,
|
||||||
|
) + PADDING,
|
||||||
|
bottom: PADDING,
|
||||||
|
left: Math.max(sidebarRect?.right ?? 0, 0) + PADDING,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
top: toolbarBottom,
|
top: toolbarBottom + PADDING,
|
||||||
right: sidebarWidth,
|
right: Math.max(
|
||||||
bottom: 0,
|
this.state.width -
|
||||||
left: propertiesPanelWidth,
|
(sidebarRect?.left ?? this.state.width) +
|
||||||
|
PADDING,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
bottom: PADDING,
|
||||||
|
left: Math.max(propertiesPanelRect?.right ?? 0, 0) + PADDING,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3923,7 +3935,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
fitToContent: true,
|
fitToContent: true,
|
||||||
viewportZoomFactor: 0.8,
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3979,6 +3991,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scrollToContent(nextNode, {
|
this.scrollToContent(nextNode, {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4411,6 +4424,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scrollToContent(firstNode, {
|
this.scrollToContent(firstNode, {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CLASSES, EVENT } from "../constants";
|
||||||
import { useStable } from "../hooks/useStable";
|
import { useStable } from "../hooks/useStable";
|
||||||
|
|
||||||
import "./SearchMenu.scss";
|
import "./SearchMenu.scss";
|
||||||
|
import { round } from "../../math";
|
||||||
|
|
||||||
const searchQueryAtom = atom<string>("");
|
const searchQueryAtom = atom<string>("");
|
||||||
export const searchItemInFocusAtom = atom<number | null>(null);
|
export const searchItemInFocusAtom = atom<number | null>(null);
|
||||||
|
@ -154,16 +155,23 @@ export const SearchMenu = () => {
|
||||||
const match = searchMatches.items[focusIndex];
|
const match = searchMatches.items[focusIndex];
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
|
const zoomValue = app.state.zoom.value;
|
||||||
|
|
||||||
const matchAsElement = newTextElement({
|
const matchAsElement = newTextElement({
|
||||||
text: match.searchQuery,
|
text: match.searchQuery,
|
||||||
x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0),
|
x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0),
|
||||||
y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0),
|
y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0),
|
||||||
width: match.matchedLines[0]?.width,
|
width: match.matchedLines[0]?.width,
|
||||||
height: match.matchedLines[0]?.height,
|
height: match.matchedLines[0]?.height,
|
||||||
|
fontSize: match.textElement.fontSize,
|
||||||
|
fontFamily: match.textElement.fontFamily,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const FONT_SIZE_LEGIBILITY_THRESHOLD = 14;
|
||||||
|
|
||||||
|
const fontSize = match.textElement.fontSize;
|
||||||
const isTextTiny =
|
const isTextTiny =
|
||||||
match.textElement.fontSize * app.state.zoom.value < 12;
|
fontSize * zoomValue < FONT_SIZE_LEGIBILITY_THRESHOLD;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isElementCompletelyInViewport(
|
!isElementCompletelyInViewport(
|
||||||
|
@ -184,9 +192,17 @@ export const SearchMenu = () => {
|
||||||
) {
|
) {
|
||||||
let zoomOptions: Parameters<AppClassProperties["scrollToContent"]>[1];
|
let zoomOptions: Parameters<AppClassProperties["scrollToContent"]>[1];
|
||||||
|
|
||||||
if (isTextTiny && app.state.zoom.value >= 1) {
|
if (isTextTiny) {
|
||||||
zoomOptions = { fitToViewport: true };
|
if (fontSize >= FONT_SIZE_LEGIBILITY_THRESHOLD) {
|
||||||
} else if (isTextTiny || app.state.zoom.value > 1) {
|
zoomOptions = { fitToContent: true };
|
||||||
|
} else {
|
||||||
|
zoomOptions = {
|
||||||
|
fitToViewport: true,
|
||||||
|
// calculate zoom level to make the fontSize ~equal to FONT_SIZE_THRESHOLD, rounded to nearest 10%
|
||||||
|
maxZoom: round(FONT_SIZE_LEGIBILITY_THRESHOLD / fontSize, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
zoomOptions = { fitToContent: true };
|
zoomOptions = { fitToContent: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +210,7 @@ export const SearchMenu = () => {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
...zoomOptions,
|
...zoomOptions,
|
||||||
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,7 +223,6 @@ export const newTextElement = (
|
||||||
verticalAlign?: VerticalAlign;
|
verticalAlign?: VerticalAlign;
|
||||||
containerId?: ExcalidrawTextContainer["id"] | null;
|
containerId?: ExcalidrawTextContainer["id"] | null;
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
|
||||||
autoResize?: ExcalidrawTextElement["autoResize"];
|
autoResize?: ExcalidrawTextElement["autoResize"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import type { AppState, Zoom } from "../types";
|
import type { AppState, Offsets, Zoom } from "../types";
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||||
import { viewportCoordsToSceneCoords } from "../utils";
|
import { viewportCoordsToSceneCoords } from "../utils";
|
||||||
|
|
||||||
|
@ -67,12 +67,7 @@ export const isElementCompletelyInViewport = (
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
},
|
},
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
padding?: Partial<{
|
padding?: Offsets,
|
||||||
top: number;
|
|
||||||
right: number;
|
|
||||||
bottom: number;
|
|
||||||
left: number;
|
|
||||||
}>,
|
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
|
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
|
||||||
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AppState, PointerCoords, Zoom } from "../types";
|
import type { AppState, Offsets, PointerCoords, Zoom } from "../types";
|
||||||
import type { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import {
|
import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
|
@ -31,14 +31,28 @@ export const centerScrollOn = ({
|
||||||
scenePoint,
|
scenePoint,
|
||||||
viewportDimensions,
|
viewportDimensions,
|
||||||
zoom,
|
zoom,
|
||||||
|
offsets,
|
||||||
}: {
|
}: {
|
||||||
scenePoint: PointerCoords;
|
scenePoint: PointerCoords;
|
||||||
viewportDimensions: { height: number; width: number };
|
viewportDimensions: { height: number; width: number };
|
||||||
zoom: Zoom;
|
zoom: Zoom;
|
||||||
|
offsets?: Offsets;
|
||||||
}) => {
|
}) => {
|
||||||
|
let scrollX =
|
||||||
|
(viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value -
|
||||||
|
scenePoint.x;
|
||||||
|
|
||||||
|
scrollX += (offsets?.left ?? 0) / 2 / zoom.value;
|
||||||
|
|
||||||
|
let scrollY =
|
||||||
|
(viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value -
|
||||||
|
scenePoint.y;
|
||||||
|
|
||||||
|
scrollY += (offsets?.top ?? 0) / 2 / zoom.value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollX: viewportDimensions.width / 2 / zoom.value - scenePoint.x,
|
scrollX,
|
||||||
scrollY: viewportDimensions.height / 2 / zoom.value - scenePoint.y,
|
scrollY,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -851,3 +851,10 @@ export type GenerateDiagramToCode = (props: {
|
||||||
frame: ExcalidrawMagicFrameElement;
|
frame: ExcalidrawMagicFrameElement;
|
||||||
children: readonly ExcalidrawElement[];
|
children: readonly ExcalidrawElement[];
|
||||||
}) => MaybePromise<{ html: string }>;
|
}) => MaybePromise<{ html: string }>;
|
||||||
|
|
||||||
|
export type Offsets = Partial<{
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
}>;
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
export const PRECISION = 10e-5;
|
export const PRECISION = 10e-5;
|
||||||
|
|
||||||
export function clamp(value: number, min: number, max: number) {
|
export const clamp = (value: number, min: number, max: number) => {
|
||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function round(value: number, precision: number) {
|
export const round = (
|
||||||
|
value: number,
|
||||||
|
precision: number,
|
||||||
|
func: "round" | "floor" | "ceil" = "round",
|
||||||
|
) => {
|
||||||
const multiplier = Math.pow(10, precision);
|
const multiplier = Math.pow(10, precision);
|
||||||
|
|
||||||
return Math.round((value + Number.EPSILON) * multiplier) / multiplier;
|
return Math[func]((value + Number.EPSILON) * multiplier) / multiplier;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const roundToStep = (
|
||||||
|
value: number,
|
||||||
|
step: number,
|
||||||
|
func: "round" | "floor" | "ceil" = "round",
|
||||||
|
): number => {
|
||||||
|
const factor = 1 / step;
|
||||||
|
return Math[func](value * factor) / factor;
|
||||||
|
};
|
||||||
|
|
||||||
export const average = (a: number, b: number) => (a + b) / 2;
|
export const average = (a: number, b: number) => (a + b) / 2;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue