diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 1f21de9cf..9f585d2e1 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -953,6 +953,7 @@ class App extends React.Component { normalizedWidth, normalizedHeight, this.state, + this.scene.getNonDeletedElementsMap(), ); const hasBeenInitialized = this.initializedEmbeds.has(el.id); @@ -1287,6 +1288,7 @@ class App extends React.Component { scrollY: this.state.scrollY, zoom: this.state.zoom, }, + this.scene.getNonDeletedElementsMap(), ) ) { // if frame not visible, don't render its name diff --git a/packages/excalidraw/element/bounds.test.ts b/packages/excalidraw/element/bounds.test.ts index 253137b07..e495343f7 100644 --- a/packages/excalidraw/element/bounds.test.ts +++ b/packages/excalidraw/element/bounds.test.ts @@ -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); diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index 7eb7fa48a..e7c6f7fb3 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -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 { diff --git a/packages/excalidraw/element/sizeHelpers.ts b/packages/excalidraw/element/sizeHelpers.ts index 1b69ca0bc..e30ea9877 100644 --- a/packages/excalidraw/element/sizeHelpers.ts +++ b/packages/excalidraw/element/sizeHelpers.ts @@ -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, diff --git a/packages/excalidraw/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts index 0e9fce164..62c59b6f8 100644 --- a/packages/excalidraw/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -886,7 +886,7 @@ const _renderInteractiveScene = ({ let scrollBars; if (renderConfig.renderScrollbars) { scrollBars = getScrollBars( - elementsMap, + visibleElements, normalizedWidth, normalizedHeight, appState, diff --git a/packages/excalidraw/scene/Renderer.ts b/packages/excalidraw/scene/Renderer.ts index 1593d6d2e..0875b9f05 100644 --- a/packages/excalidraw/scene/Renderer.ts +++ b/packages/excalidraw/scene/Renderer.ts @@ -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); } diff --git a/packages/excalidraw/scene/scrollbars.ts b/packages/excalidraw/scene/scrollbars.ts index 14009588b..0e0e6d0ab 100644 --- a/packages/excalidraw/scene/scrollbars.ts +++ b/packages/excalidraw/scene/scrollbars.ts @@ -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, diff --git a/packages/excalidraw/scene/selection.ts b/packages/excalidraw/scene/selection.ts index 27d4db1c9..3c3df898e 100644 --- a/packages/excalidraw/scene/selection.ts +++ b/packages/excalidraw/scene/selection.ts @@ -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; diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 3061c02d4..bc83d0057 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -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); diff --git a/packages/excalidraw/tests/clipboard.test.tsx b/packages/excalidraw/tests/clipboard.test.tsx index 149ebcd1e..1a6fe47be 100644 --- a/packages/excalidraw/tests/clipboard.test.tsx +++ b/packages/excalidraw/tests/clipboard.test.tsx @@ -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); }); }); diff --git a/packages/utils/withinBounds.ts b/packages/utils/withinBounds.ts index 637cab3e1..6a380d9c3 100644 --- a/packages/utils/withinBounds.ts +++ b/packages/utils/withinBounds.ts @@ -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,