mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
fix: make bounds independent of scene (#7679)
* fix: make bounds independent of scene * pass only elements to getCommonBounds * lint * pass elementsMap to getVisibleAndNonSelectedElements
This commit is contained in:
parent
9013c84524
commit
79d9dc2f8f
@ -953,6 +953,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
normalizedWidth,
|
||||
normalizedHeight,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const hasBeenInitialized = this.initializedEmbeds.has(el.id);
|
||||
|
||||
@ -1287,6 +1288,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
scrollY: this.state.scrollY,
|
||||
zoom: this.state.zoom,
|
||||
},
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
// if frame not visible, don't render its name
|
||||
|
@ -62,9 +62,15 @@ describe("getElementAbsoluteCoords", () => {
|
||||
|
||||
describe("getElementBounds", () => {
|
||||
it("rectangle", () => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(
|
||||
_ce({ x: 40, y: 30, w: 20, h: 10, a: Math.PI / 4, t: "rectangle" }),
|
||||
);
|
||||
const element = _ce({
|
||||
x: 40,
|
||||
y: 30,
|
||||
w: 20,
|
||||
h: 10,
|
||||
a: Math.PI / 4,
|
||||
t: "rectangle",
|
||||
});
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
|
||||
expect(x1).toEqual(39.39339828220179);
|
||||
expect(y1).toEqual(24.393398282201787);
|
||||
expect(x2).toEqual(60.60660171779821);
|
||||
@ -72,9 +78,17 @@ describe("getElementBounds", () => {
|
||||
});
|
||||
|
||||
it("diamond", () => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(
|
||||
_ce({ x: 40, y: 30, w: 20, h: 10, a: Math.PI / 4, t: "diamond" }),
|
||||
);
|
||||
const element = _ce({
|
||||
x: 40,
|
||||
y: 30,
|
||||
w: 20,
|
||||
h: 10,
|
||||
a: Math.PI / 4,
|
||||
t: "diamond",
|
||||
});
|
||||
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
|
||||
|
||||
expect(x1).toEqual(42.928932188134524);
|
||||
expect(y1).toEqual(27.928932188134524);
|
||||
expect(x2).toEqual(57.071067811865476);
|
||||
@ -82,9 +96,16 @@ describe("getElementBounds", () => {
|
||||
});
|
||||
|
||||
it("ellipse", () => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(
|
||||
_ce({ x: 40, y: 30, w: 20, h: 10, a: Math.PI / 4, t: "ellipse" }),
|
||||
);
|
||||
const element = _ce({
|
||||
x: 40,
|
||||
y: 30,
|
||||
w: 20,
|
||||
h: 10,
|
||||
a: Math.PI / 4,
|
||||
t: "ellipse",
|
||||
});
|
||||
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
|
||||
expect(x1).toEqual(42.09430584957905);
|
||||
expect(y1).toEqual(27.09430584957905);
|
||||
expect(x2).toEqual(57.90569415042095);
|
||||
@ -92,7 +113,7 @@ describe("getElementBounds", () => {
|
||||
});
|
||||
|
||||
it("curved line", () => {
|
||||
const [x1, y1, x2, y2] = getElementBounds({
|
||||
const element = {
|
||||
..._ce({
|
||||
t: "line",
|
||||
x: 449.58203125,
|
||||
@ -106,7 +127,9 @@ describe("getElementBounds", () => {
|
||||
[67.33984375, 92.48828125] as [number, number],
|
||||
[-102.7890625, 52.15625] as [number, number],
|
||||
],
|
||||
} as ExcalidrawLinearElement);
|
||||
} as ExcalidrawLinearElement;
|
||||
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
|
||||
expect(x1).toEqual(360.3176068760539);
|
||||
expect(y1).toEqual(185.90654264413516);
|
||||
expect(x2).toEqual(480.87005902729743);
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
ExcalidrawFreeDrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMapOrArray,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import { distance2d, rotate, rotatePoint } from "../math";
|
||||
@ -25,7 +24,7 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import Scene from "../scene/Scene";
|
||||
import { arrayToMap } from "../utils";
|
||||
|
||||
export type RectangleBox = {
|
||||
x: number;
|
||||
@ -63,7 +62,7 @@ export class ElementBounds {
|
||||
}
|
||||
>();
|
||||
|
||||
static getBounds(element: ExcalidrawElement) {
|
||||
static getBounds(element: ExcalidrawElement, elementsMap: ElementsMap) {
|
||||
const cachedBounds = ElementBounds.boundsCache.get(element);
|
||||
|
||||
if (
|
||||
@ -75,23 +74,12 @@ export class ElementBounds {
|
||||
) {
|
||||
return cachedBounds.bounds;
|
||||
}
|
||||
const scene = Scene.getScene(element);
|
||||
const bounds = ElementBounds.calculateBounds(
|
||||
element,
|
||||
scene?.getNonDeletedElementsMap() || new Map(),
|
||||
);
|
||||
const bounds = ElementBounds.calculateBounds(element, elementsMap);
|
||||
|
||||
// hack to ensure that downstream checks could retrieve element Scene
|
||||
// so as to have correctly calculated bounds
|
||||
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
|
||||
const shouldCache = !!scene;
|
||||
|
||||
if (shouldCache) {
|
||||
ElementBounds.boundsCache.set(element, {
|
||||
version: element.version,
|
||||
bounds,
|
||||
});
|
||||
}
|
||||
ElementBounds.boundsCache.set(element, {
|
||||
version: element.version,
|
||||
bounds,
|
||||
});
|
||||
|
||||
return bounds;
|
||||
}
|
||||
@ -748,11 +736,17 @@ const getLinearElementRotatedBounds = (
|
||||
return coords;
|
||||
};
|
||||
|
||||
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
||||
return ElementBounds.getBounds(element);
|
||||
export const getElementBounds = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): Bounds => {
|
||||
return ElementBounds.getBounds(element, elementsMap);
|
||||
};
|
||||
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
|
||||
if ("size" in elements ? !elements.size : !elements.length) {
|
||||
|
||||
export const getCommonBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): Bounds => {
|
||||
if (!elements.length) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
@ -761,8 +755,10 @@ export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
const elementsMap = arrayToMap(elements);
|
||||
|
||||
elements.forEach((element) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(element);
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
||||
minX = Math.min(minX, x1);
|
||||
minY = Math.min(minY, y1);
|
||||
maxX = Math.max(maxX, x2);
|
||||
@ -868,9 +864,9 @@ export const getClosestElementBounds = (
|
||||
|
||||
let minDistance = Infinity;
|
||||
let closestElement = elements[0];
|
||||
|
||||
const elementsMap = arrayToMap(elements);
|
||||
elements.forEach((element) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(element);
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
||||
const distance = distance2d((x1 + x2) / 2, (y1 + y2) / 2, from.x, from.y);
|
||||
|
||||
if (distance < minDistance) {
|
||||
@ -879,7 +875,7 @@ export const getClosestElementBounds = (
|
||||
}
|
||||
});
|
||||
|
||||
return getElementBounds(closestElement);
|
||||
return getElementBounds(closestElement, elementsMap);
|
||||
};
|
||||
|
||||
export interface BoundingBox {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { ElementsMap, ExcalidrawElement } from "./types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||
@ -26,8 +26,9 @@ export const isElementInViewport = (
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
|
||||
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: viewTransformations.offsetLeft,
|
||||
|
@ -886,7 +886,7 @@ const _renderInteractiveScene = ({
|
||||
let scrollBars;
|
||||
if (renderConfig.renderScrollbars) {
|
||||
scrollBars = getScrollBars(
|
||||
elementsMap,
|
||||
visibleElements,
|
||||
normalizedWidth,
|
||||
normalizedHeight,
|
||||
appState,
|
||||
|
@ -40,13 +40,19 @@ export class Renderer {
|
||||
const visibleElements: NonDeletedExcalidrawElement[] = [];
|
||||
for (const element of elementsMap.values()) {
|
||||
if (
|
||||
isElementInViewport(element, width, height, {
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
})
|
||||
isElementInViewport(
|
||||
element,
|
||||
width,
|
||||
height,
|
||||
{
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
},
|
||||
elementsMap,
|
||||
)
|
||||
) {
|
||||
visibleElements.push(element);
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
import { getCommonBounds } from "../element";
|
||||
import { InteractiveCanvasAppState } from "../types";
|
||||
import { RenderableElementsMap, ScrollBars } from "./types";
|
||||
import { ScrollBars } from "./types";
|
||||
import { getGlobalCSSVariable } from "../utils";
|
||||
import { getLanguage } from "../i18n";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
|
||||
export const SCROLLBAR_MARGIN = 4;
|
||||
export const SCROLLBAR_WIDTH = 6;
|
||||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||
|
||||
export const getScrollBars = (
|
||||
elements: RenderableElementsMap,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): ScrollBars => {
|
||||
if (!elements.size) {
|
||||
if (!elements.length) {
|
||||
return {
|
||||
horizontal: null,
|
||||
vertical: null,
|
||||
|
@ -52,12 +52,17 @@ export const getElementsWithinSelection = (
|
||||
getElementAbsoluteCoords(selection, elementsMap);
|
||||
|
||||
let elementsInSelection = elements.filter((element) => {
|
||||
let [elementX1, elementY1, elementX2, elementY2] =
|
||||
getElementBounds(element);
|
||||
let [elementX1, elementY1, elementX2, elementY2] = getElementBounds(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const containingFrame = getContainingFrame(element);
|
||||
if (containingFrame) {
|
||||
const [fx1, fy1, fx2, fy2] = getElementBounds(containingFrame);
|
||||
const [fx1, fy1, fx2, fy2] = getElementBounds(
|
||||
containingFrame,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
elementX1 = Math.max(fx1, elementX1);
|
||||
elementY1 = Math.max(fy1, elementY1);
|
||||
@ -97,6 +102,7 @@ export const getVisibleAndNonSelectedElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const selectedElementsSet = new Set(
|
||||
selectedElements.map((element) => element.id),
|
||||
@ -107,6 +113,7 @@ export const getVisibleAndNonSelectedElements = (
|
||||
appState.width,
|
||||
appState.height,
|
||||
appState,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
return !selectedElementsSet.has(element.id) && isVisible;
|
||||
|
@ -269,6 +269,7 @@ const getReferenceElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const selectedFrames = selectedElements
|
||||
.filter((element) => isFrameLikeElement(element))
|
||||
@ -278,6 +279,7 @@ const getReferenceElements = (
|
||||
elements,
|
||||
selectedElements,
|
||||
appState,
|
||||
elementsMap,
|
||||
).filter(
|
||||
(element) => !(element.frameId && selectedFrames.includes(element.frameId)),
|
||||
);
|
||||
@ -293,6 +295,7 @@ export const getVisibleGaps = (
|
||||
elements,
|
||||
selectedElements,
|
||||
appState,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const referenceBounds = getMaximumGroups(referenceElements, elementsMap)
|
||||
@ -580,6 +583,7 @@ export const getReferenceSnapPoints = (
|
||||
elements,
|
||||
selectedElements,
|
||||
appState,
|
||||
elementsMap,
|
||||
);
|
||||
return getMaximumGroups(referenceElements, elementsMap)
|
||||
.filter(
|
||||
@ -1296,6 +1300,7 @@ export const getSnapLinesAtPointer = (
|
||||
elements,
|
||||
[],
|
||||
appState,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const snapDistance = getSnapDistance(appState.zoom.value);
|
||||
|
@ -12,6 +12,7 @@ import { getElementBounds } from "../element";
|
||||
import { NormalizedZoomValue } from "../types";
|
||||
import { API } from "./helpers/api";
|
||||
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
||||
import { arrayToMap } from "../utils";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
@ -138,6 +139,8 @@ describe("paste text as single lines", () => {
|
||||
});
|
||||
|
||||
it("should space items correctly", async () => {
|
||||
const elementsMap = arrayToMap(h.elements);
|
||||
|
||||
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
|
||||
const lineHeightPx =
|
||||
getLineHeightInPx(
|
||||
@ -149,16 +152,17 @@ describe("paste text as single lines", () => {
|
||||
pasteWithCtrlCmdV(text);
|
||||
await waitFor(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
||||
const [fx, firstElY] = getElementBounds(h.elements[0], elementsMap);
|
||||
for (let i = 1; i < h.elements.length; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [fx, elY] = getElementBounds(h.elements[i]);
|
||||
const [fx, elY] = getElementBounds(h.elements[i], elementsMap);
|
||||
expect(elY).toEqual(firstElY + lineHeightPx * i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should leave a space for blank new lines", async () => {
|
||||
const elementsMap = arrayToMap(h.elements);
|
||||
const text = "hkhkjhki\n\njgkjhffjh";
|
||||
const lineHeightPx =
|
||||
getLineHeightInPx(
|
||||
@ -168,11 +172,12 @@ describe("paste text as single lines", () => {
|
||||
10 / h.app.state.zoom.value;
|
||||
mouse.moveTo(100, 100);
|
||||
pasteWithCtrlCmdV(text);
|
||||
|
||||
await waitFor(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
||||
const [fx, firstElY] = getElementBounds(h.elements[0], elementsMap);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [lx, lastElY] = getElementBounds(h.elements[1]);
|
||||
const [lx, lastElY] = getElementBounds(h.elements[1], elementsMap);
|
||||
expect(lastElY).toEqual(firstElY + lineHeightPx * 2);
|
||||
});
|
||||
});
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
import { isValueInRange, rotatePoint } from "../excalidraw/math";
|
||||
import type { Point } from "../excalidraw/types";
|
||||
import { Bounds, getElementBounds } from "../excalidraw/element/bounds";
|
||||
import { arrayToMap } from "../excalidraw/utils";
|
||||
|
||||
type Element = NonDeletedExcalidrawElement;
|
||||
type Elements = readonly NonDeletedExcalidrawElement[];
|
||||
@ -158,7 +159,7 @@ export const elementsOverlappingBBox = ({
|
||||
type: "overlap" | "contain" | "inside";
|
||||
}) => {
|
||||
if (isExcalidrawElement(bounds)) {
|
||||
bounds = getElementBounds(bounds);
|
||||
bounds = getElementBounds(bounds, arrayToMap(elements));
|
||||
}
|
||||
const adjustedBBox: Bounds = [
|
||||
bounds[0] - errorMargin,
|
||||
|
Loading…
Reference in New Issue
Block a user