diff --git a/packages/excalidraw/actions/actionAlign.tsx b/packages/excalidraw/actions/actionAlign.tsx index 137f68ae9f..8d7d362172 100644 --- a/packages/excalidraw/actions/actionAlign.tsx +++ b/packages/excalidraw/actions/actionAlign.tsx @@ -40,8 +40,13 @@ const alignSelectedElements = ( alignment: Alignment, ) => { const selectedElements = app.scene.getSelectedElements(appState); + const elementsMap = arrayToMap(elements); - const updatedElements = alignElements(selectedElements, alignment); + const updatedElements = alignElements( + selectedElements, + elementsMap, + alignment, + ); const updatedElementsMap = arrayToMap(updatedElements); diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index b421695446..05dd9c786f 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -45,8 +45,9 @@ export const actionUnbindText = register({ }, perform: (elements, appState, _, app) => { const selectedElements = app.scene.getSelectedElements(appState); + const elementsMap = app.scene.getNonDeletedElementsMap(); selectedElements.forEach((element) => { - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { const { width, height, baseline } = measureText( boundTextElement.originalText, @@ -106,7 +107,10 @@ export const actionBindText = register({ if ( textElement && bindingContainer && - getBoundTextElement(bindingContainer) === null + getBoundTextElement( + bindingContainer, + app.scene.getNonDeletedElementsMap(), + ) === null ) { return true; } diff --git a/packages/excalidraw/actions/actionDistribute.tsx b/packages/excalidraw/actions/actionDistribute.tsx index bf51bedf4b..be48bc8708 100644 --- a/packages/excalidraw/actions/actionDistribute.tsx +++ b/packages/excalidraw/actions/actionDistribute.tsx @@ -32,7 +32,11 @@ const distributeSelectedElements = ( ) => { const selectedElements = app.scene.getSelectedElements(appState); - const updatedElements = distributeElements(selectedElements, distribution); + const updatedElements = distributeElements( + selectedElements, + app.scene.getNonDeletedElementsMap(), + distribution, + ); const updatedElementsMap = arrayToMap(updatedElements); diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index ba079168e9..7126f549ef 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -139,7 +139,7 @@ const duplicateElements = ( continue; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, arrayToMap(elements)); const isElementAFrameLike = isFrameLikeElement(element); if (idsOfElementsToDuplicate.get(element.id)) { diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts index 81476e2411..c760af44d6 100644 --- a/packages/excalidraw/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -5,6 +5,7 @@ import { ExcalidrawElement, NonDeleted, NonDeletedElementsMap, + NonDeletedSceneElementsMap, } from "../element/types"; import { resizeMultipleElements } from "../element/resizeElements"; import { AppState } from "../types"; @@ -67,7 +68,7 @@ export const actionFlipVertical = register({ const flipSelectedElements = ( elements: readonly ExcalidrawElement[], - elementsMap: NonDeletedElementsMap, + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, appState: Readonly, flipDirection: "horizontal" | "vertical", ) => { @@ -96,7 +97,7 @@ const flipSelectedElements = ( const flipElements = ( selectedElements: NonDeleted[], - elementsMap: NonDeletedElementsMap, + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, appState: AppState, flipDirection: "horizontal" | "vertical", ): ExcalidrawElement[] => { diff --git a/packages/excalidraw/actions/actionGroup.tsx b/packages/excalidraw/actions/actionGroup.tsx index 42bd26efea..44523857ae 100644 --- a/packages/excalidraw/actions/actionGroup.tsx +++ b/packages/excalidraw/actions/actionGroup.tsx @@ -105,7 +105,10 @@ export const actionGroup = register({ const frameElementsMap = groupByFrameLikes(selectedElements); frameElementsMap.forEach((elementsInFrame, frameId) => { - removeElementsFromFrame(elementsInFrame); + removeElementsFromFrame( + elementsInFrame, + app.scene.getNonDeletedElementsMap(), + ); }); } @@ -225,6 +228,7 @@ export const actionUngroup = register({ nextElements, getElementsInResizingFrame(nextElements, frame, appState), frame, + app, ); } }); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index c2a47802ff..79e50aa68e 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -606,7 +606,7 @@ export const actionChangeFontSize = register({ perform: (elements, appState, value, app) => { return changeFontSize(elements, appState, app, () => value, value); }, - PanelComponent: ({ elements, appState, updateData }) => ( + PanelComponent: ({ elements, appState, updateData, app }) => (
{t("labels.fontSize")} - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => hasSelection ? null @@ -738,7 +745,7 @@ export const actionChangeFontFamily = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { const options: { value: FontFamilyValues; text: string; @@ -778,14 +785,21 @@ export const actionChangeFontFamily = register({ if (isTextElement(element)) { return element.fontFamily; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); if (boundTextElement) { return boundTextElement.fontFamily; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => hasSelection ? null @@ -830,7 +844,8 @@ export const actionChangeTextAlign = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { + const elementsMap = app.scene.getNonDeletedElementsMap(); return (
{t("labels.textAlign")} @@ -863,14 +878,18 @@ export const actionChangeTextAlign = register({ if (isTextElement(element)) { return element.textAlign; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + elementsMap, + ); if (boundTextElement) { return boundTextElement.textAlign; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement(element, elementsMap) !== null, (hasSelection) => hasSelection ? null : appState.currentItemTextAlign, )} @@ -913,7 +932,7 @@ export const actionChangeVerticalAlign = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { return (
@@ -945,14 +964,21 @@ export const actionChangeVerticalAlign = register({ if (isTextElement(element) && element.containerId) { return element.verticalAlign; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); if (boundTextElement) { return boundTextElement.verticalAlign; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE), )} onChange={(value) => updateData(value)} diff --git a/packages/excalidraw/actions/actionStyles.ts b/packages/excalidraw/actions/actionStyles.ts index 9c6589bbc7..25a6baf2a5 100644 --- a/packages/excalidraw/actions/actionStyles.ts +++ b/packages/excalidraw/actions/actionStyles.ts @@ -32,12 +32,15 @@ export let copiedStyles: string = "{}"; export const actionCopyStyles = register({ name: "copyStyles", trackEvent: { category: "element" }, - perform: (elements, appState) => { + perform: (elements, appState, formData, app) => { const elementsCopied = []; const element = elements.find((el) => appState.selectedElementIds[el.id]); elementsCopied.push(element); if (element && hasBoundTextElement(element)) { - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); elementsCopied.push(boundTextElement); } if (element) { @@ -59,7 +62,7 @@ export const actionCopyStyles = register({ export const actionPasteStyles = register({ name: "pasteStyles", trackEvent: { category: "element" }, - perform: (elements, appState) => { + perform: (elements, appState, formData, app) => { const elementsCopied = JSON.parse(copiedStyles); const pastedElement = elementsCopied[0]; const boundTextElement = elementsCopied[1]; diff --git a/packages/excalidraw/align.ts b/packages/excalidraw/align.ts index 06382838f7..90ecabb117 100644 --- a/packages/excalidraw/align.ts +++ b/packages/excalidraw/align.ts @@ -1,4 +1,4 @@ -import { ExcalidrawElement } from "./element/types"; +import { ElementsMap, ExcalidrawElement } from "./element/types"; import { newElementWith } from "./element/mutateElement"; import { BoundingBox, getCommonBoundingBox } from "./element/bounds"; import { getMaximumGroups } from "./groups"; @@ -10,10 +10,13 @@ export interface Alignment { export const alignElements = ( selectedElements: ExcalidrawElement[], + elementsMap: ElementsMap, alignment: Alignment, ): ExcalidrawElement[] => { - const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements); - + const groups: ExcalidrawElement[][] = getMaximumGroups( + selectedElements, + elementsMap, + ); const selectionBoundingBox = getCommonBoundingBox(selectedElements); return groups.flatMap((group) => { diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index d67c8893d3..c11d64d041 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -1,6 +1,10 @@ import { useState } from "react"; import { ActionManager } from "../actions/manager"; -import { ExcalidrawElementType, NonDeletedElementsMap } from "../element/types"; +import { + ExcalidrawElementType, + NonDeletedElementsMap, + NonDeletedSceneElementsMap, +} from "../element/types"; import { t } from "../i18n"; import { useDevice } from "./App"; import { @@ -47,7 +51,7 @@ export const SelectedShapeActions = ({ renderAction, }: { appState: UIAppState; - elementsMap: NonDeletedElementsMap; + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap; renderAction: ActionManager["renderAction"]; }) => { const targetElements = getTargetElements(elementsMap, appState); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 1618cd2aed..30f86c24ee 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1431,6 +1431,8 @@ class App extends React.Component { pendingImageElementId: this.state.pendingImageElementId, }); + const allElementsMap = this.scene.getNonDeletedElementsMap(); + const shouldBlockPointerEvents = !( this.state.editingElement && isLinearElement(this.state.editingElement) @@ -1628,6 +1630,7 @@ class App extends React.Component { canvas={this.canvas} rc={this.rc} elementsMap={elementsMap} + allElementsMap={allElementsMap} visibleElements={visibleElements} versionNonce={versionNonce} selectionNonce={ @@ -3869,7 +3872,11 @@ class App extends React.Component { if (!isTextElement(selectedElement)) { container = selectedElement as ExcalidrawTextContainer; } - const midPoint = getContainerCenter(selectedElement, this.state); + const midPoint = getContainerCenter( + selectedElement, + this.state, + this.scene.getNonDeletedElementsMap(), + ); const sceneX = midPoint.x; const sceneY = midPoint.y; this.startTextEditing({ @@ -4333,6 +4340,7 @@ class App extends React.Component { this.frameNameBoundsCache, x, y, + this.scene.getNonDeletedElementsMap(), ) ? allHitElements[allHitElements.length - 2] : elementWithHighestZIndex; @@ -4362,7 +4370,14 @@ class App extends React.Component { ); return getElementsAtPosition(elements, (element) => - hitTest(element, this.state, this.frameNameBoundsCache, x, y), + hitTest( + element, + this.state, + this.frameNameBoundsCache, + x, + y, + this.scene.getNonDeletedElementsMap(), + ), ).filter((element) => { // hitting a frame's element from outside the frame is not considered a hit const containingFrame = getContainingFrame(element); @@ -4399,7 +4414,10 @@ class App extends React.Component { container, ); if (container && parentCenterPosition) { - const boundTextElementToContainer = getBoundTextElement(container); + const boundTextElementToContainer = getBoundTextElement( + container, + this.scene.getNonDeletedElementsMap(), + ); if (!boundTextElementToContainer) { shouldBindToContainer = true; } @@ -4412,7 +4430,10 @@ class App extends React.Component { if (isTextElement(selectedElements[0])) { existingTextElement = selectedElements[0]; } else if (container) { - existingTextElement = getBoundTextElement(selectedElements[0]); + existingTextElement = getBoundTextElement( + selectedElements[0], + this.scene.getNonDeletedElementsMap(), + ); } else { existingTextElement = this.getTextElementAtPosition(sceneX, sceneY); } @@ -4621,7 +4642,11 @@ class App extends React.Component { [sceneX, sceneY], ) ) { - const midPoint = getContainerCenter(container, this.state); + const midPoint = getContainerCenter( + container, + this.state, + this.scene.getNonDeletedElementsMap(), + ); sceneX = midPoint.x; sceneY = midPoint.y; @@ -5257,8 +5282,8 @@ class App extends React.Component { const element = LinearElementEditor.getElement( linearElementEditor.elementId, ); - - const boundTextElement = getBoundTextElement(element); + const elementsMap = this.scene.getNonDeletedElementsMap(); + const boundTextElement = getBoundTextElement(element, elementsMap); if (!element) { return; @@ -5285,6 +5310,7 @@ class App extends React.Component { linearElementEditor, { x: scenePointerX, y: scenePointerY }, this.state, + this.scene.getNonDeletedElementsMap(), ); if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) { @@ -5300,6 +5326,7 @@ class App extends React.Component { this.frameNameBoundsCache, scenePointerX, scenePointerY, + elementsMap, ) ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); @@ -5311,6 +5338,7 @@ class App extends React.Component { this.frameNameBoundsCache, scenePointerX, scenePointerY, + this.scene.getNonDeletedElementsMap(), ) ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); @@ -6060,6 +6088,7 @@ class App extends React.Component { this.history, pointerDownState.origin, linearElementEditor, + this.scene.getNonDeletedElementsMap(), ); if (ret.hitElement) { pointerDownState.hit.element = ret.hitElement; @@ -6995,6 +7024,7 @@ class App extends React.Component { ); }, linearElementEditor, + this.scene.getNonDeletedElementsMap(), ); if (didDrag) { pointerDownState.lastCoords.x = pointerCoords.x; @@ -7713,7 +7743,10 @@ class App extends React.Component { groupIds: [], }); - removeElementsFromFrame([linearElement]); + removeElementsFromFrame( + [linearElement], + this.scene.getNonDeletedElementsMap(), + ); this.scene.informMutation(); } @@ -7866,6 +7899,7 @@ class App extends React.Component { this.state, ), frame, + this, ); } @@ -8093,6 +8127,7 @@ class App extends React.Component { this.frameNameBoundsCache, pointerDownState.origin.x, pointerDownState.origin.y, + this.scene.getNonDeletedElementsMap(), )) || (!hitElement && pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements)) @@ -9334,7 +9369,11 @@ class App extends React.Component { let elementCenterX = container.x + container.width / 2; let elementCenterY = container.y + container.height / 2; - const elementCenter = getContainerCenter(container, appState); + const elementCenter = getContainerCenter( + container, + appState, + this.scene.getNonDeletedElementsMap(), + ); if (elementCenter) { elementCenterX = elementCenter.x; elementCenterY = elementCenter.y; diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx index 3dc5b91751..bfdb669e6c 100644 --- a/packages/excalidraw/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -7,13 +7,17 @@ import type { RenderableElementsMap, StaticCanvasRenderConfig, } from "../../scene/types"; -import type { NonDeletedExcalidrawElement } from "../../element/types"; +import type { + NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, +} from "../../element/types"; import { isRenderThrottlingEnabled } from "../../reactUtils"; type StaticCanvasProps = { canvas: HTMLCanvasElement; rc: RoughCanvas; elementsMap: RenderableElementsMap; + allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; versionNonce: number | undefined; selectionNonce: number | undefined; @@ -67,6 +71,7 @@ const StaticCanvas = (props: StaticCanvasProps) => { rc: props.rc, scale: props.scale, elementsMap: props.elementsMap, + allElementsMap: props.allElementsMap, visibleElements: props.visibleElements, appState: props.appState, renderConfig: props.renderConfig, diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts index 7b5286923c..8ce842300c 100644 --- a/packages/excalidraw/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -24,6 +24,7 @@ import { normalizeText, } from "../element/textElement"; import { + ElementsMap, ExcalidrawArrowElement, ExcalidrawBindableElement, ExcalidrawElement, @@ -42,7 +43,7 @@ import { VerticalAlign, } from "../element/types"; import { MarkOptional } from "../utility-types"; -import { assertNever, cloneJSON, getFontString } from "../utils"; +import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils"; import { getSizeFromPoints } from "../points"; import { randomId } from "../random"; @@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100; const bindTextToContainer = ( container: ExcalidrawElement, textProps: { text: string } & MarkOptional, + elementsMap: ElementsMap, ) => { const textElement: ExcalidrawTextElement = newTextElement({ x: 0, @@ -623,6 +625,7 @@ export const convertToExcalidrawElements = ( let [container, text] = bindTextToContainer( excalidrawElement, element?.label, + arrayToMap(elementStore.getElements()), ); elementStore.add(container); elementStore.add(text); diff --git a/packages/excalidraw/distribute.ts b/packages/excalidraw/distribute.ts index acad09b2de..368b2f24da 100644 --- a/packages/excalidraw/distribute.ts +++ b/packages/excalidraw/distribute.ts @@ -1,7 +1,7 @@ -import { ExcalidrawElement } from "./element/types"; import { newElementWith } from "./element/mutateElement"; import { getMaximumGroups } from "./groups"; import { getCommonBoundingBox } from "./element/bounds"; +import type { ElementsMap, ExcalidrawElement } from "./element/types"; export interface Distribution { space: "between"; @@ -10,6 +10,7 @@ export interface Distribution { export const distributeElements = ( selectedElements: ExcalidrawElement[], + elementsMap: ElementsMap, distribution: Distribution, ): ExcalidrawElement[] => { const [start, mid, end, extent] = @@ -18,7 +19,7 @@ export const distributeElements = ( : (["minY", "midY", "maxY", "height"] as const); const bounds = getCommonBoundingBox(selectedElements); - const groups = getMaximumGroups(selectedElements) + const groups = getMaximumGroups(selectedElements, elementsMap) .map((group) => [group, getCommonBoundingBox(group)] as const) .sort((a, b) => a[1][mid] - b[1][mid]); diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index 3f6cf0022d..66d29f3f63 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -321,9 +321,9 @@ export const updateBoundElements = ( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( simultaneouslyUpdated, ); - + const scene = Scene.getScene(changedElement)!; getNonDeletedElements( - Scene.getScene(changedElement)!, + scene, boundLinearElements.map((el) => el.id), ).forEach((element) => { if (!isLinearElement(element)) { @@ -362,9 +362,12 @@ export const updateBoundElements = ( endBinding, changedElement as ExcalidrawBindableElement, ); - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement( + element, + scene.getNonDeletedElementsMap(), + ); if (boundText) { - handleBindTextResize(element, false); + handleBindTextResize(element, scene.getNonDeletedElementsMap(), false); } }); }; diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index 673649e5f4..f892089f75 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -6,6 +6,7 @@ import { NonDeleted, ExcalidrawTextElementWithContainer, ElementsMapOrArray, + ElementsMap, } from "./types"; import { distance2d, rotate, rotatePoint } from "../math"; import rough from "roughjs/bin/rough"; @@ -74,13 +75,16 @@ export class ElementBounds { ) { return cachedBounds.bounds; } - - const bounds = ElementBounds.calculateBounds(element); + const scene = Scene.getScene(element); + const bounds = ElementBounds.calculateBounds( + element, + scene?.getNonDeletedElementsMap() || new Map(), + ); // 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.getScene(element); + const shouldCache = !!scene; if (shouldCache) { ElementBounds.boundsCache.set(element, { @@ -92,7 +96,10 @@ export class ElementBounds { return bounds; } - private static calculateBounds(element: ExcalidrawElement): Bounds { + private static calculateBounds( + element: ExcalidrawElement, + elementsMap: ElementsMap, + ): Bounds { let bounds: Bounds; const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); @@ -111,7 +118,7 @@ export class ElementBounds { maxY + element.y, ]; } else if (isLinearElement(element)) { - bounds = getLinearElementRotatedBounds(element, cx, cy); + bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap); } else if (element.type === "diamond") { const [x11, y11] = rotate(cx, y1, cx, cy, element.angle); const [x12, y12] = rotate(cx, y2, cx, cy, element.angle); @@ -154,16 +161,17 @@ export const getElementAbsoluteCoords = ( element: ExcalidrawElement, includeBoundText: boolean = false, ): [number, number, number, number, number, number] => { + const elementsMap = + Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map(); if (isFreeDrawElement(element)) { return getFreeDrawElementAbsoluteCoords(element); } else if (isLinearElement(element)) { return LinearElementEditor.getElementAbsoluteCoords( element, + elementsMap, includeBoundText, ); } else if (isTextElement(element)) { - const elementsMap = - Scene.getScene(element)?.getElementsMapIncludingDeleted(); const container = elementsMap ? getContainerElement(element, elementsMap) : null; @@ -677,7 +685,10 @@ const getLinearElementRotatedBounds = ( element: ExcalidrawLinearElement, cx: number, cy: number, + elementsMap: ElementsMap, ): Bounds => { + const boundTextElement = getBoundTextElement(element, elementsMap); + if (element.points.length < 2) { const [pointX, pointY] = element.points[0]; const [x, y] = rotate( @@ -689,7 +700,6 @@ const getLinearElementRotatedBounds = ( ); let coords: Bounds = [x, y, x, y]; - const boundTextElement = getBoundTextElement(element); if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, @@ -714,7 +724,6 @@ const getLinearElementRotatedBounds = ( rotate(element.x + x, element.y + y, cx, cy, element.angle); const res = getMinMaxXYFromCurvePathOps(ops, transformXY); let coords: Bounds = [res[0], res[1], res[2], res[3]]; - const boundTextElement = getBoundTextElement(element); if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index 709781b229..b8c07e3ab0 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -28,6 +28,7 @@ import { StrokeRoundness, ExcalidrawFrameLikeElement, ExcalidrawIframeLikeElement, + ElementsMap, } from "./types"; import { @@ -78,6 +79,7 @@ export const hitTest = ( frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, + elementsMap: ElementsMap, ): boolean => { // How many pixels off the shape boundary we still consider a hit const threshold = 10 / appState.zoom.value; @@ -95,7 +97,7 @@ export const hitTest = ( ); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { const isHittingBoundTextElement = hitTest( boundTextElement, @@ -103,6 +105,7 @@ export const hitTest = ( frameNameBoundsCache, x, y, + elementsMap, ); if (isHittingBoundTextElement) { return true; @@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = ( frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, + elementsMap: ElementsMap, ): boolean => { const threshold = 10 / appState.zoom.value; // So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element // eg for linear elements text can be outside the element bounding box - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if ( boundTextElement && - hitTest(boundTextElement, appState, frameNameBoundsCache, x, y) + hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap) ) { return false; } diff --git a/packages/excalidraw/element/dragElements.ts b/packages/excalidraw/element/dragElements.ts index ecec4d0831..0144f55a40 100644 --- a/packages/excalidraw/element/dragElements.ts +++ b/packages/excalidraw/element/dragElements.ts @@ -57,7 +57,10 @@ export const dragSelectedElements = ( // skip arrow labels since we calculate its position during render !isArrowElement(element) ) { - const textElement = getBoundTextElement(element); + const textElement = getBoundTextElement( + element, + scene.getNonDeletedElementsMap(), + ); if (textElement) { updateElementCoords(pointerDownState, textElement, adjustedOffset); } diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index bf64ee7321..5c3c6acaa2 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -5,6 +5,7 @@ import { PointBinding, ExcalidrawBindableElement, ExcalidrawTextElementWithContainer, + ElementsMap, } from "./types"; import { distance2d, @@ -193,6 +194,7 @@ export class LinearElementEditor { pointSceneCoords: { x: number; y: number }[], ) => void, linearElementEditor: LinearElementEditor, + elementsMap: ElementsMap, ): boolean { if (!linearElementEditor) { return false; @@ -272,9 +274,9 @@ export class LinearElementEditor { ); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { - handleBindTextResize(element, false); + handleBindTextResize(element, elementsMap, false); } // suggest bindings for first and last point if selected @@ -404,9 +406,10 @@ export class LinearElementEditor { static getEditorMidPoints = ( element: NonDeleted, + elementsMap: ElementsMap, appState: InteractiveCanvasAppState, ): typeof editorMidPointsCache["points"] => { - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); // Since its not needed outside editor unless 2 pointer lines or bound text if ( @@ -465,6 +468,7 @@ export class LinearElementEditor { linearElementEditor: LinearElementEditor, scenePointer: { x: number; y: number }, appState: AppState, + elementsMap: ElementsMap, ) => { const { elementId } = linearElementEditor; const element = LinearElementEditor.getElement(elementId); @@ -503,7 +507,7 @@ export class LinearElementEditor { } let index = 0; const midPoints: typeof editorMidPointsCache["points"] = - LinearElementEditor.getEditorMidPoints(element, appState); + LinearElementEditor.getEditorMidPoints(element, elementsMap, appState); while (index < midPoints.length) { if (midPoints[index] !== null) { const distance = distance2d( @@ -581,6 +585,7 @@ export class LinearElementEditor { linearElementEditor: LinearElementEditor, appState: AppState, midPoint: Point, + elementsMap: ElementsMap, ) { const element = LinearElementEditor.getElement( linearElementEditor.elementId, @@ -588,7 +593,11 @@ export class LinearElementEditor { if (!element) { return -1; } - const midPoints = LinearElementEditor.getEditorMidPoints(element, appState); + const midPoints = LinearElementEditor.getEditorMidPoints( + element, + elementsMap, + appState, + ); let index = 0; while (index < midPoints.length) { if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) { @@ -605,6 +614,7 @@ export class LinearElementEditor { history: History, scenePointer: { x: number; y: number }, linearElementEditor: LinearElementEditor, + elementsMap: ElementsMap, ): { didAddPoint: boolean; hitElement: NonDeleted | null; @@ -630,6 +640,7 @@ export class LinearElementEditor { linearElementEditor, scenePointer, appState, + elementsMap, ); let segmentMidpointIndex = null; if (segmentMidpoint) { @@ -637,6 +648,7 @@ export class LinearElementEditor { linearElementEditor, appState, segmentMidpoint, + elementsMap, ); } if (event.altKey && appState.editingLinearElement) { @@ -1418,6 +1430,7 @@ export class LinearElementEditor { static getElementAbsoluteCoords = ( element: ExcalidrawLinearElement, + elementsMap: ElementsMap, includeBoundText: boolean = false, ): [number, number, number, number, number, number] => { let coords: [number, number, number, number, number, number]; @@ -1462,7 +1475,7 @@ export class LinearElementEditor { if (!includeBoundText) { return coords; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { coords = LinearElementEditor.getMinMaxXYWithBoundText( element, diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index 3158c064cf..447a07993f 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -342,7 +342,7 @@ export const refreshTextDimensions = ( text = wrapText( text, getFontString(textElement), - getBoundTextMaxWidth(container), + getBoundTextMaxWidth(container, textElement), ); } const dimensions = getAdjustedDimensions(textElement, text); diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 46b891aca5..deb5fead32 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -126,6 +126,7 @@ export const transformElements = ( rotateMultipleElements( originalElements, selectedElements, + elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle, @@ -219,7 +220,7 @@ const measureFontSizeFromWidth = ( if (hasContainer) { const container = getContainerElement(element, elementsMap); if (container) { - width = getBoundTextMaxWidth(container); + width = getBoundTextMaxWidth(container, element); } } const nextFontSize = element.fontSize * (nextWidth / width); @@ -394,7 +395,7 @@ export const resizeSingleElement = ( let scaleY = atStartBoundsHeight / boundsCurrentHeight; let boundTextFont: { fontSize?: number; baseline?: number } = {}; - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (transformHandleDirection.includes("e")) { scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth; @@ -458,7 +459,7 @@ export const resizeSingleElement = ( const nextFont = measureFontSizeFromWidth( boundTextElement, elementsMap, - getBoundTextMaxWidth(updatedElement), + getBoundTextMaxWidth(updatedElement, boundTextElement), getBoundTextMaxHeight(updatedElement, boundTextElement), ); if (nextFont === null) { @@ -640,6 +641,7 @@ export const resizeSingleElement = ( } handleBindTextResize( element, + elementsMap, transformHandleDirection, shouldMaintainAspectRatio, ); @@ -882,7 +884,7 @@ export const resizeMultipleElements = ( newSize: { width, height }, }); - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement && boundTextFontSize) { mutateElement( boundTextElement, @@ -892,7 +894,7 @@ export const resizeMultipleElements = ( }, false, ); - handleBindTextResize(element, transformHandleType, true); + handleBindTextResize(element, elementsMap, transformHandleType, true); } } @@ -902,6 +904,7 @@ export const resizeMultipleElements = ( const rotateMultipleElements = ( originalElements: PointerDownState["originalElements"], elements: readonly NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, pointerX: number, pointerY: number, shouldRotateWithDiscreteAngle: boolean, @@ -941,7 +944,7 @@ const rotateMultipleElements = ( ); updateBoundElements(element, { simultaneouslyUpdated: elements }); - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); if (boundText && !isArrowElement(element)) { mutateElement( boundText, diff --git a/packages/excalidraw/element/textElement.test.ts b/packages/excalidraw/element/textElement.test.ts index b6221336d1..2f3a2dcc75 100644 --- a/packages/excalidraw/element/textElement.test.ts +++ b/packages/excalidraw/element/textElement.test.ts @@ -319,17 +319,17 @@ describe("Test measureText", () => { it("should return max width when container is rectangle", () => { const container = API.createElement({ type: "rectangle", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(168); + expect(getBoundTextMaxWidth(container, null)).toBe(168); }); it("should return max width when container is ellipse", () => { const container = API.createElement({ type: "ellipse", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(116); + expect(getBoundTextMaxWidth(container, null)).toBe(116); }); it("should return max width when container is diamond", () => { const container = API.createElement({ type: "diamond", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(79); + expect(getBoundTextMaxWidth(container, null)).toBe(79); }); }); diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index da1348ec2d..b264c0d594 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -23,7 +23,6 @@ import { VERTICAL_ALIGN, } from "../constants"; import { MaybeTransformHandleType } from "./transformHandles"; -import Scene from "../scene/Scene"; import { isTextElement } from "."; import { isBoundToContainer, isArrowElement } from "./typeChecks"; import { LinearElementEditor } from "./linearElementEditor"; @@ -89,7 +88,7 @@ export const redrawTextBoundingBox = ( container, textElement as ExcalidrawTextElementWithContainer, ); - const maxContainerWidth = getBoundTextMaxWidth(container); + const maxContainerWidth = getBoundTextMaxWidth(container, textElement); if (!isArrowElement(container) && metrics.height > maxContainerHeight) { const nextHeight = computeContainerDimensionForBoundText( @@ -162,6 +161,7 @@ export const bindTextToShapeAfterDuplication = ( export const handleBindTextResize = ( container: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, transformHandleType: MaybeTransformHandleType, shouldMaintainAspectRatio = false, ) => { @@ -170,25 +170,17 @@ export const handleBindTextResize = ( return; } resetOriginalContainerCache(container.id); - let textElement = Scene.getScene(container)!.getElement( - boundTextElementId, - ) as ExcalidrawTextElement; + const textElement = getBoundTextElement(container, elementsMap); if (textElement && textElement.text) { if (!container) { return; } - textElement = Scene.getScene(container)!.getElement( - boundTextElementId, - ) as ExcalidrawTextElement; let text = textElement.text; let nextHeight = textElement.height; let nextWidth = textElement.width; - const maxWidth = getBoundTextMaxWidth(container); - const maxHeight = getBoundTextMaxHeight( - container, - textElement as ExcalidrawTextElementWithContainer, - ); + const maxWidth = getBoundTextMaxWidth(container, textElement); + const maxHeight = getBoundTextMaxHeight(container, textElement); let containerHeight = container.height; let nextBaseLine = textElement.baseline; if ( @@ -243,10 +235,7 @@ export const handleBindTextResize = ( if (!isArrowElement(container)) { mutateElement( textElement, - computeBoundTextPosition( - container, - textElement as ExcalidrawTextElementWithContainer, - ), + computeBoundTextPosition(container, textElement), ); } } @@ -264,7 +253,7 @@ export const computeBoundTextPosition = ( } const containerCoords = getContainerCoords(container); const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement); - const maxContainerWidth = getBoundTextMaxWidth(container); + const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement); let x; let y; @@ -667,17 +656,18 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => { : null; }; -export const getBoundTextElement = (element: ExcalidrawElement | null) => { +export const getBoundTextElement = ( + element: ExcalidrawElement | null, + elementsMap: ElementsMap, +) => { if (!element) { return null; } const boundTextElementId = getBoundTextElementId(element); + if (boundTextElementId) { - return ( - (Scene.getScene(element)?.getElement( - boundTextElementId, - ) as ExcalidrawTextElementWithContainer) || null - ); + return (elementsMap.get(boundTextElementId) || + null) as ExcalidrawTextElementWithContainer | null; } return null; }; @@ -699,6 +689,7 @@ export const getContainerElement = ( export const getContainerCenter = ( container: ExcalidrawElement, appState: AppState, + elementsMap: ElementsMap, ) => { if (!isArrowElement(container)) { return { @@ -718,6 +709,7 @@ export const getContainerCenter = ( const index = container.points.length / 2 - 1; let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints( container, + elementsMap, appState, )[index]; if (!midSegmentMidpoint) { @@ -877,9 +869,7 @@ export const computeContainerDimensionForBoundText = ( export const getBoundTextMaxWidth = ( container: ExcalidrawElement, - boundTextElement: ExcalidrawTextElement | null = getBoundTextElement( - container, - ), + boundTextElement: ExcalidrawTextElement | null, ) => { const { width } = container; if (isArrowElement(container)) { diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 801f0c4405..d12d34f89a 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -34,6 +34,7 @@ import { computeContainerDimensionForBoundText, detectLineHeight, computeBoundTextPosition, + getBoundTextElement, } from "./textElement"; import { actionDecreaseFontSize, @@ -196,7 +197,8 @@ export const textWysiwyg = ({ } } - maxWidth = getBoundTextMaxWidth(container); + maxWidth = getBoundTextMaxWidth(container, updatedTextElement); + maxHeight = getBoundTextMaxHeight( container, updatedTextElement as ExcalidrawTextElementWithContainer, @@ -361,10 +363,14 @@ export const textWysiwyg = ({ fontFamily: app.state.currentItemFontFamily, }); if (container) { + const boundTextElement = getBoundTextElement( + container, + app.scene.getNonDeletedElementsMap(), + ); const wrappedText = wrapText( `${editable.value}${data}`, font, - getBoundTextMaxWidth(container), + getBoundTextMaxWidth(container, boundTextElement), ); const width = getTextWidth(wrappedText, font); editable.style.width = `${width}px`; diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index 7659ad1e90..f89e8d5f23 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -279,6 +279,16 @@ export type NonDeletedElementsMap = Map< export type SceneElementsMap = Map & MakeBrand<"SceneElementsMap">; +/** + * Map of all non-deleted Scene elements. + * Not a subset. Use this type when you need access to current Scene elements. + */ +export type NonDeletedSceneElementsMap = Map< + ExcalidrawElement["id"], + NonDeletedExcalidrawElement +> & + MakeBrand<"NonDeletedSceneElementsMap">; + export type ElementsMapOrArray = | readonly ExcalidrawElement[] | Readonly; diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index ecb70ef1e4..1457c4ecf7 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -444,6 +444,7 @@ export const addElementsToFrame = ( elementsToAdd: NonDeletedExcalidrawElement[], frame: ExcalidrawFrameLikeElement, ): T => { + const elementsMap = arrayToMap(allElements); const currTargetFrameChildrenMap = new Map(); for (const element of allElements.values()) { if (element.frameId === frame.id) { @@ -481,7 +482,7 @@ export const addElementsToFrame = ( finalElementsToAdd.push(element); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if ( boundTextElement && !suppliedElementsToAddSet.has(boundTextElement.id) && @@ -506,6 +507,7 @@ export const addElementsToFrame = ( export const removeElementsFromFrame = ( elementsToRemove: ReadonlySetLike, + elementsMap: ElementsMap, ) => { const _elementsToRemove = new Map< ExcalidrawElement["id"], @@ -524,7 +526,7 @@ export const removeElementsFromFrame = ( const arr = toRemoveElementsByFrame.get(element.frameId) || []; arr.push(element); - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { _elementsToRemove.set(boundTextElement.id, boundTextElement); arr.push(boundTextElement); @@ -550,7 +552,7 @@ export const removeAllElementsFromFrame = ( frame: ExcalidrawFrameLikeElement, ) => { const elementsInFrame = getFrameChildren(allElements, frame.id); - removeElementsFromFrame(elementsInFrame); + removeElementsFromFrame(elementsInFrame, arrayToMap(allElements)); return allElements; }; @@ -558,6 +560,7 @@ export const replaceAllElementsInFrame = ( allElements: readonly T[], nextElementsInFrame: ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, + app: AppClassProperties, ): T[] => { return addElementsToFrame( removeAllElementsFromFrame(allElements, frame), @@ -608,7 +611,7 @@ export const updateFrameMembershipOfSelectedElements = < }); if (elementsToRemove.size > 0) { - removeElementsFromFrame(elementsToRemove); + removeElementsFromFrame(elementsToRemove, elementsMap); } return allElements; }; diff --git a/packages/excalidraw/groups.ts b/packages/excalidraw/groups.ts index b0bedc4f95..f8c0eddb93 100644 --- a/packages/excalidraw/groups.ts +++ b/packages/excalidraw/groups.ts @@ -4,6 +4,7 @@ import { NonDeleted, NonDeletedExcalidrawElement, ElementsMapOrArray, + ElementsMap, } from "./element/types"; import { AppClassProperties, @@ -329,12 +330,12 @@ export const removeFromSelectedGroups = ( export const getMaximumGroups = ( elements: ExcalidrawElement[], + elementsMap: ElementsMap, ): ExcalidrawElement[][] => { const groups: Map = new Map< String, ExcalidrawElement[] >(); - elements.forEach((element: ExcalidrawElement) => { const groupId = element.groupIds.length === 0 @@ -344,7 +345,7 @@ export const getMaximumGroups = ( const currentGroupMembers = groups.get(groupId) || []; // Include bound text if present when grouping - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { currentGroupMembers.push(boundTextElement); } diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index 39e6c49749..5ab3f3ca52 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -6,6 +6,7 @@ import { ExcalidrawImageElement, ExcalidrawTextElementWithContainer, ExcalidrawFrameLikeElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { isTextElement, @@ -190,6 +191,7 @@ const cappedElementCanvasSize = ( const generateElementCanvas = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, zoom: Zoom, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, @@ -247,7 +249,8 @@ const generateElementCanvas = ( zoomValue: zoom.value, canvasOffsetX, canvasOffsetY, - boundTextElementVersion: getBoundTextElement(element)?.version || null, + boundTextElementVersion: + getBoundTextElement(element, elementsMap)?.version || null, containingFrameOpacity: getContainingFrame(element)?.opacity || 100, }; }; @@ -407,6 +410,7 @@ export const elementWithCanvasCache = new WeakMap< const generateElementWithCanvas = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, ) => { @@ -416,7 +420,9 @@ const generateElementWithCanvas = ( prevElementWithCanvas && prevElementWithCanvas.zoomValue !== zoom.value && !appState?.shouldCacheIgnoreZoom; - const boundTextElementVersion = getBoundTextElement(element)?.version || null; + const boundTextElementVersion = + getBoundTextElement(element, elementsMap)?.version || null; + const containingFrameOpacity = getContainingFrame(element)?.opacity || 100; if ( @@ -428,6 +434,7 @@ const generateElementWithCanvas = ( ) { const elementWithCanvas = generateElementCanvas( element, + elementsMap, zoom, renderConfig, appState, @@ -445,6 +452,7 @@ const drawElementFromCanvas = ( context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, + allElementsMap: NonDeletedSceneElementsMap, ) => { const element = elementWithCanvas.element; const padding = getCanvasPadding(element); @@ -464,7 +472,8 @@ const drawElementFromCanvas = ( context.save(); context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio); - const boundTextElement = getBoundTextElement(element); + + const boundTextElement = getBoundTextElement(element, allElementsMap); if (isArrowElement(element) && boundTextElement) { const tempCanvas = document.createElement("canvas"); @@ -511,7 +520,6 @@ const drawElementFromCanvas = ( offsetY - padding * zoom; tempCanvasContext.translate(-shiftX, -shiftY); - // Clear the bound text area tempCanvasContext.clearRect( -(boundTextElement.width / 2 + BOUND_TEXT_PADDING) * @@ -573,6 +581,7 @@ const drawElementFromCanvas = ( ) { const textElement = getBoundTextElement( element, + allElementsMap, ) as ExcalidrawTextElementWithContainer; const coords = getContainerCoords(element); context.strokeStyle = "#c92a2a"; @@ -580,7 +589,7 @@ const drawElementFromCanvas = ( context.strokeRect( (coords.x + appState.scrollX) * window.devicePixelRatio, (coords.y + appState.scrollY) * window.devicePixelRatio, - getBoundTextMaxWidth(element) * window.devicePixelRatio, + getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio, getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio, ); } @@ -616,6 +625,7 @@ export const renderSelectionElement = ( export const renderElement = ( element: NonDeletedExcalidrawElement, elementsMap: RenderableElementsMap, + allElementsMap: NonDeletedSceneElementsMap, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, @@ -687,6 +697,7 @@ export const renderElement = ( } else { const elementWithCanvas = generateElementWithCanvas( element, + elementsMap, renderConfig, appState, ); @@ -695,6 +706,7 @@ export const renderElement = ( context, renderConfig, appState, + allElementsMap, ); } @@ -737,7 +749,7 @@ export const renderElement = ( if (shouldResetImageFilter(element, renderConfig, appState)) { context.filter = "none"; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (isArrowElement(element) && boundTextElement) { const tempCanvas = document.createElement("canvas"); @@ -820,6 +832,7 @@ export const renderElement = ( } else { const elementWithCanvas = generateElementWithCanvas( element, + elementsMap, renderConfig, appState, ); @@ -851,6 +864,7 @@ export const renderElement = ( context, renderConfig, appState, + allElementsMap, ); // reset @@ -1096,7 +1110,7 @@ export const renderElementToSvg = ( } case "line": case "arrow": { - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask"); if (boundText) { maskPath.setAttribute("id", `mask-${element.id}`); diff --git a/packages/excalidraw/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts index 6c358591b6..0fa56829fb 100644 --- a/packages/excalidraw/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -246,6 +246,7 @@ const renderLinearPointHandles = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, element: NonDeleted, + elementsMap: RenderableElementsMap, ) => { if (!appState.selectedLinearElement) { return; @@ -269,6 +270,7 @@ const renderLinearPointHandles = ( //Rendering segment mid points const midPoints = LinearElementEditor.getEditorMidPoints( element, + elementsMap, appState, ).filter((midPoint) => midPoint !== null) as Point[]; @@ -485,7 +487,12 @@ const _renderInteractiveScene = ({ }); if (editingLinearElement) { - renderLinearPointHandles(context, appState, editingLinearElement); + renderLinearPointHandles( + context, + appState, + editingLinearElement, + elementsMap, + ); } // Paint selection element @@ -528,6 +535,7 @@ const _renderInteractiveScene = ({ context, appState, selectedElements[0] as NonDeleted, + elementsMap, ); } @@ -553,6 +561,7 @@ const _renderInteractiveScene = ({ context, appState, selectedElements[0] as ExcalidrawLinearElement, + elementsMap, ); } const selectionColor = renderConfig.selectionColor || oc.black; @@ -891,6 +900,7 @@ const _renderStaticScene = ({ canvas, rc, elementsMap, + allElementsMap, visibleElements, scale, appState, @@ -972,6 +982,7 @@ const _renderStaticScene = ({ renderElement( element, elementsMap, + allElementsMap, rc, context, renderConfig, @@ -982,6 +993,7 @@ const _renderStaticScene = ({ renderElement( element, elementsMap, + allElementsMap, rc, context, renderConfig, @@ -1005,6 +1017,7 @@ const _renderStaticScene = ({ renderElement( element, elementsMap, + allElementsMap, rc, context, renderConfig, @@ -1024,6 +1037,7 @@ const _renderStaticScene = ({ renderElement( label, elementsMap, + allElementsMap, rc, context, renderConfig, diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts index 326f98c7fb..88c3d89963 100644 --- a/packages/excalidraw/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -4,8 +4,8 @@ import { NonDeleted, ExcalidrawFrameLikeElement, ElementsMapOrArray, - NonDeletedElementsMap, SceneElementsMap, + NonDeletedSceneElementsMap, } from "../element/types"; import { isNonDeletedElement } from "../element"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -27,7 +27,7 @@ type SelectionHash = string & { __brand: "selectionHash" }; const getNonDeletedElements = ( allElements: readonly T[], ) => { - const elementsMap = new Map() as NonDeletedElementsMap; + const elementsMap = new Map() as NonDeletedSceneElementsMap; const elements: T[] = []; for (const element of allElements) { if (!element.isDeleted) { @@ -120,8 +120,9 @@ class Scene { private callbacks: Set = new Set(); private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = []; - private nonDeletedElementsMap: NonDeletedElementsMap = - new Map() as NonDeletedElementsMap; + private nonDeletedElementsMap = toBrandedType( + new Map(), + ); private elements: readonly ExcalidrawElement[] = []; private nonDeletedFramesLikes: readonly NonDeleted[] = []; diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index 9f1f12a22f..d463e25971 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -4,6 +4,7 @@ import { ExcalidrawFrameLikeElement, ExcalidrawTextElement, NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { Bounds, @@ -248,14 +249,15 @@ export const exportToCanvas = async ( files, }); - const elementsMap = toBrandedType( - arrayToMap(elementsForRender), - ); - renderStaticScene({ canvas, rc: rough.canvas(canvas), - elementsMap, + elementsMap: toBrandedType( + arrayToMap(elementsForRender), + ), + allElementsMap: toBrandedType( + arrayToMap(elements), + ), visibleElements: elementsForRender, scale, appState: { diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 957b080b38..02aa3b7bf7 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -4,6 +4,7 @@ import { ExcalidrawTextElement, NonDeletedElementsMap, NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { AppClassProperties, @@ -66,6 +67,7 @@ export type StaticSceneRenderConfig = { canvas: HTMLCanvasElement; rc: RoughCanvas; elementsMap: RenderableElementsMap; + allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; scale: number; appState: StaticCanvasAppState; diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index e7ff9b7876..7557145ae9 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -16,6 +16,7 @@ import { KEYS } from "./keys"; import { rangeIntersection, rangesOverlap, rotatePoint } from "./math"; import { getVisibleAndNonSelectedElements } from "./scene/selection"; import { AppState, KeyboardModifiersObject, Point } from "./types"; +import { arrayToMap } from "./utils"; const SNAP_DISTANCE = 8; @@ -286,7 +287,10 @@ export const getVisibleGaps = ( appState, ); - const referenceBounds = getMaximumGroups(referenceElements) + const referenceBounds = getMaximumGroups( + referenceElements, + arrayToMap(elements), + ) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), @@ -572,7 +576,7 @@ export const getReferenceSnapPoints = ( appState, ); - return getMaximumGroups(referenceElements) + return getMaximumGroups(referenceElements, arrayToMap(elements)) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx index f4ddeafd29..ce0e1c856b 100644 --- a/packages/excalidraw/tests/linearElementEditor.test.tsx +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -24,6 +24,7 @@ import { import * as textElementUtils from "../element/textElement"; import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { vi } from "vitest"; +import { arrayToMap } from "../utils"; const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene"); const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); @@ -307,6 +308,7 @@ describe("Test Linear Elements", () => { const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); @@ -320,6 +322,7 @@ describe("Test Linear Elements", () => { const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints( h.elements[0] as ExcalidrawLinearElement, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]); @@ -351,7 +354,11 @@ describe("Test Linear Elements", () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); expect([line.x, line.y]).toEqual(points[0]); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const startPoint = centerPoint(points[0], midPoints[0] as Point); const deltaX = 50; @@ -373,6 +380,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(midPoints[0]).not.toEqual(newMidPoints[0]); @@ -458,7 +466,11 @@ describe("Test Linear Elements", () => { it("should update only the first segment midpoint when its point is dragged", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -478,6 +490,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); @@ -487,7 +500,11 @@ describe("Test Linear Elements", () => { it("should hide midpoints in the segment when points moved close", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -507,6 +524,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); // This midpoint is hidden since the points are too close @@ -526,7 +544,11 @@ describe("Test Linear Elements", () => { ]); expect(line.points.length).toEqual(4); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); // delete 3rd point deletePoint(points[2]); @@ -538,6 +560,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(newMidPoints.length).toEqual(2); @@ -615,7 +638,11 @@ describe("Test Linear Elements", () => { it("should update all the midpoints when its point is dragged", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -630,6 +657,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); @@ -651,7 +679,11 @@ describe("Test Linear Elements", () => { it("should hide midpoints in the segment when points moved close", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -671,6 +703,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); // This mid point is hidden due to point being too close @@ -685,7 +718,11 @@ describe("Test Linear Elements", () => { ]); expect(line.points.length).toEqual(4); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const points = LinearElementEditor.getPointsGlobalCoordinates(line); // delete 3rd point @@ -694,6 +731,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(newMidPoints.length).toEqual(2); @@ -762,7 +800,7 @@ describe("Test Linear Elements", () => { type: "text", x: 0, y: 0, - text: wrapText(text, font, getBoundTextMaxWidth(container)), + text: wrapText(text, font, getBoundTextMaxWidth(container, null)), containerId: container.id, width: 30, height: 20, @@ -986,8 +1024,13 @@ describe("Test Linear Elements", () => { collaboration made easy" `); - expect(LinearElementEditor.getElementAbsoluteCoords(container, true)) - .toMatchInlineSnapshot(` + expect( + LinearElementEditor.getElementAbsoluteCoords( + container, + h.app.scene.getNonDeletedElementsMap(), + true, + ), + ).toMatchInlineSnapshot(` [ 20, 20, @@ -1020,8 +1063,13 @@ describe("Test Linear Elements", () => { "Online whiteboard collaboration made easy" `); - expect(LinearElementEditor.getElementAbsoluteCoords(container, true)) - .toMatchInlineSnapshot(` + expect( + LinearElementEditor.getElementAbsoluteCoords( + container, + h.app.scene.getNonDeletedElementsMap(), + true, + ), + ).toMatchInlineSnapshot(` [ 20, 35, @@ -1121,7 +1169,11 @@ describe("Test Linear Elements", () => { expect(rect.x).toBe(400); expect(rect.y).toBe(0); expect( - wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), + wrapText( + textElement.originalText, + font, + getBoundTextMaxWidth(arrow, null), + ), ).toMatchInlineSnapshot(` "Online whiteboard collaboration made easy" @@ -1140,11 +1192,17 @@ describe("Test Linear Elements", () => { expect(rect.x).toBe(200); expect(rect.y).toBe(0); expect(handleBindTextResizeSpy).toHaveBeenCalledWith( - h.elements[1], + h.elements[0], + arrayToMap(h.elements), + "nw", false, ); expect( - wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), + wrapText( + textElement.originalText, + font, + getBoundTextMaxWidth(arrow, null), + ), ).toMatchInlineSnapshot(` "Online whiteboard collaboration made