From 47f87f4ecbc62058b6e2b6b7d953952bf6f3ecaf Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 16 Feb 2024 11:35:01 +0530 Subject: [PATCH] fix: remove scene from getElementAbsoluteCoords and dependent functions and use elementsMap (#7663) * fix: remove scene from getElementAbsoluteCoords and dependent functions and use elementsMap * lint * fix * use non deleted elements where possible * use non deleted elements map in actions * pass elementsMap instead of array to elementOverlapsWithFrame * lint * fix * pass elementsMap to getElementsCorners * pass elementsMap to getEligibleElementsForBinding * pass elementsMap in bindOrUnbindSelectedElements and unbindLinearElements * pass elementsMap in elementsAreInFrameBounds,elementOverlapsWithFrame,isCursorInFrame,getElementsInResizingFrame * pass elementsMap in getElementsWithinSelection, getElementsCompletelyInFrame, isElementContainingFrame, getElementsInNewFrame * pass elementsMap to getElementWithTransformHandleType * pass elementsMap to getVisibleGaps, getMaximumGroups,getReferenceSnapPoints,snapDraggedElements * lint * pass elementsMap to bindTextToShapeAfterDuplication,bindLinearElementToElement,getTextBindableContainerAtPosition * revert changes for bindTextToShapeAfterDuplication --- .../excalidraw/actions/actionBoundText.tsx | 18 ++- .../excalidraw/actions/actionFinalize.tsx | 7 +- packages/excalidraw/actions/actionFlip.ts | 2 +- packages/excalidraw/actions/actionGroup.tsx | 9 +- .../excalidraw/actions/actionProperties.tsx | 4 + packages/excalidraw/actions/actionStyles.ts | 6 +- packages/excalidraw/components/App.tsx | 128 +++++++++++++----- packages/excalidraw/data/restore.ts | 1 + packages/excalidraw/data/transform.ts | 10 +- .../element/ElementCanvasButtons.tsx | 9 +- packages/excalidraw/element/Hyperlink.tsx | 49 +++++-- packages/excalidraw/element/binding.ts | 118 +++++++++++++--- packages/excalidraw/element/bounds.test.ts | 17 +-- packages/excalidraw/element/bounds.ts | 18 ++- packages/excalidraw/element/collision.ts | 90 +++++++++--- packages/excalidraw/element/dragElements.ts | 2 +- .../excalidraw/element/linearElementEditor.ts | 83 ++++++++++-- packages/excalidraw/element/newElement.ts | 10 +- packages/excalidraw/element/resizeElements.ts | 29 ++-- packages/excalidraw/element/resizeTest.ts | 6 +- packages/excalidraw/element/textElement.ts | 35 +++-- packages/excalidraw/element/textWysiwyg.tsx | 16 ++- .../excalidraw/element/transformHandles.ts | 5 +- packages/excalidraw/frame.ts | 86 +++++++----- packages/excalidraw/renderer/renderElement.ts | 37 +++-- packages/excalidraw/renderer/renderScene.ts | 53 ++++++-- packages/excalidraw/scene/Fonts.ts | 6 +- packages/excalidraw/scene/export.ts | 3 +- packages/excalidraw/scene/selection.ts | 6 +- packages/excalidraw/snapping.ts | 40 +++--- packages/excalidraw/tests/binding.test.tsx | 9 +- packages/excalidraw/tests/flip.test.tsx | 13 +- packages/excalidraw/tests/helpers/ui.ts | 10 +- .../tests/linearElementEditor.test.tsx | 94 ++++++++++--- packages/excalidraw/tests/move.test.tsx | 4 +- packages/excalidraw/tests/resize.test.tsx | 16 ++- 36 files changed, 779 insertions(+), 270 deletions(-) diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index 722ad51115..e0ea95cd4d 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -58,7 +58,11 @@ export const actionUnbindText = register({ element.id, ); resetOriginalContainerCache(element.id); - const { x, y } = computeBoundTextPosition(element, boundTextElement); + const { x, y } = computeBoundTextPosition( + element, + boundTextElement, + elementsMap, + ); mutateElement(boundTextElement as ExcalidrawTextElement, { containerId: null, width, @@ -145,7 +149,11 @@ export const actionBindText = register({ }), }); const originalContainerHeight = container.height; - redrawTextBoundingBox(textElement, container); + redrawTextBoundingBox( + textElement, + container, + app.scene.getNonDeletedElementsMap(), + ); // overwritting the cache with original container height so // it can be restored when unbind updateOriginalContainerCache(container.id, originalContainerHeight); @@ -286,7 +294,11 @@ export const actionWrapTextInContainer = register({ }, false, ); - redrawTextBoundingBox(textElement, container); + redrawTextBoundingBox( + textElement, + container, + app.scene.getNonDeletedElementsMap(), + ); updatedElements = pushContainerBelowText( [...updatedElements, container], diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index a7c34c5ac5..623876d586 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -1,6 +1,6 @@ import { KEYS } from "../keys"; import { isInvisiblySmallElement } from "../element"; -import { updateActiveTool } from "../utils"; +import { arrayToMap, updateActiveTool } from "../utils"; import { ToolButton } from "../components/ToolButton"; import { done } from "../components/icons"; import { t } from "../i18n"; @@ -26,6 +26,8 @@ export const actionFinalize = register({ _, { interactiveCanvas, focusContainer, scene }, ) => { + const elementsMap = arrayToMap(elements); + if (appState.editingLinearElement) { const { elementId, startBindingElement, endBindingElement } = appState.editingLinearElement; @@ -37,6 +39,7 @@ export const actionFinalize = register({ element, startBindingElement, endBindingElement, + elementsMap, ); } return { @@ -125,12 +128,14 @@ export const actionFinalize = register({ const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( multiPointElement, -1, + arrayToMap(elements), ); maybeBindLinearElement( multiPointElement, appState, Scene.getScene(multiPointElement)!, { x, y }, + elementsMap, ); } } diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts index c760af44d6..70fbe026d7 100644 --- a/packages/excalidraw/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -115,7 +115,7 @@ const flipElements = ( (isBindingEnabled(appState) ? bindOrUnbindSelectedElements - : unbindLinearElements)(selectedElements); + : unbindLinearElements)(selectedElements, elementsMap); return selectedElements; }; diff --git a/packages/excalidraw/actions/actionGroup.tsx b/packages/excalidraw/actions/actionGroup.tsx index 44523857ae..44e590bc26 100644 --- a/packages/excalidraw/actions/actionGroup.tsx +++ b/packages/excalidraw/actions/actionGroup.tsx @@ -180,6 +180,8 @@ export const actionUngroup = register({ trackEvent: { category: "element" }, perform: (elements, appState, _, app) => { const groupIds = getSelectedGroupIds(appState); + const elementsMap = arrayToMap(elements); + if (groupIds.length === 0) { return { appState, elements, commitToHistory: false }; } @@ -226,7 +228,12 @@ export const actionUngroup = register({ if (frame) { nextElements = replaceAllElementsInFrame( nextElements, - getElementsInResizingFrame(nextElements, frame, appState), + getElementsInResizingFrame( + nextElements, + frame, + appState, + elementsMap, + ), frame, app, ); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 79e50aa68e..8f2c350d68 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -209,6 +209,7 @@ const changeFontSize = ( redrawTextBoundingBox( newElement, app.scene.getContainerElement(oldElement), + app.scene.getNonDeletedElementsMap(), ); newElement = offsetElementAfterFontResize(oldElement, newElement); @@ -730,6 +731,7 @@ export const actionChangeFontFamily = register({ redrawTextBoundingBox( newElement, app.scene.getContainerElement(oldElement), + app.scene.getNonDeletedElementsMap(), ); return newElement; } @@ -829,6 +831,7 @@ export const actionChangeTextAlign = register({ redrawTextBoundingBox( newElement, app.scene.getContainerElement(oldElement), + app.scene.getNonDeletedElementsMap(), ); return newElement; } @@ -918,6 +921,7 @@ export const actionChangeVerticalAlign = register({ redrawTextBoundingBox( newElement, app.scene.getContainerElement(oldElement), + app.scene.getNonDeletedElementsMap(), ); return newElement; } diff --git a/packages/excalidraw/actions/actionStyles.ts b/packages/excalidraw/actions/actionStyles.ts index 25a6baf2a5..538375031c 100644 --- a/packages/excalidraw/actions/actionStyles.ts +++ b/packages/excalidraw/actions/actionStyles.ts @@ -128,7 +128,11 @@ export const actionPasteStyles = register({ element.id === newElement.containerId, ) || null; } - redrawTextBoundingBox(newElement, container); + redrawTextBoundingBox( + newElement, + container, + app.scene.getNonDeletedElementsMap(), + ); } if ( diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 3d3838afc1..b4410ab2b1 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1536,6 +1536,7 @@ class App extends React.Component { { isMagicFrameElement(firstSelectedElement) && ( { ?.status === "done" && ( { componentDidUpdate(prevProps: AppProps, prevState: AppState) { this.updateEmbeddables(); - if ( - !this.state.showWelcomeScreen && - !this.scene.getElementsIncludingDeleted().length - ) { + const elements = this.scene.getElementsIncludingDeleted(); + const elementsMap = this.scene.getElementsMapIncludingDeleted(); + + if (!this.state.showWelcomeScreen && !elements.length) { this.setState({ showWelcomeScreen: true }); } @@ -2756,27 +2759,21 @@ class App extends React.Component { LinearElementEditor.getPointAtIndexGlobalCoordinates( multiElement, -1, + elementsMap, ), ), + elementsMap, ); } - this.history.record(this.state, this.scene.getElementsIncludingDeleted()); + this.history.record(this.state, elements); // Do not notify consumers if we're still loading the scene. Among other // potential issues, this fixes a case where the tab isn't focused during // init, which would trigger onChange with empty elements, which would then // override whatever is in localStorage currently. if (!this.state.isLoading) { - this.props.onChange?.( - this.scene.getElementsIncludingDeleted(), - this.state, - this.files, - ); - this.onChangeEmitter.trigger( - this.scene.getElementsIncludingDeleted(), - this.state, - this.files, - ); + this.props.onChange?.(elements, this.state, this.files); + this.onChangeEmitter.trigger(elements, this.state, this.files); } } @@ -3126,7 +3123,11 @@ class App extends React.Component { newElement, this.scene.getElementsMapIncludingDeleted(), ); - redrawTextBoundingBox(newElement, container); + redrawTextBoundingBox( + newElement, + container, + this.scene.getElementsMapIncludingDeleted(), + ); } }); @@ -3836,7 +3837,7 @@ class App extends React.Component { y: element.y + offsetY, }); - updateBoundElements(element, { + updateBoundElements(element, this.scene.getNonDeletedElementsMap(), { simultaneouslyUpdated: selectedElements, }); }); @@ -4010,9 +4011,10 @@ class App extends React.Component { } if (isArrowKey(event.key)) { const selectedElements = this.scene.getSelectedElements(this.state); + const elementsMap = this.scene.getNonDeletedElementsMap(); isBindingEnabled(this.state) - ? bindOrUnbindSelectedElements(selectedElements) - : unbindLinearElements(selectedElements); + ? bindOrUnbindSelectedElements(selectedElements, elementsMap) + : unbindLinearElements(selectedElements, elementsMap); this.setState({ suggestedBindings: [] }); } }); @@ -4193,20 +4195,21 @@ class App extends React.Component { isExistingElement?: boolean; }, ) { + const elementsMap = this.scene.getElementsMapIncludingDeleted(); + const updateElement = ( text: string, originalText: string, isDeleted: boolean, ) => { this.scene.replaceAllElements([ + // Not sure why we include deleted elements as well hence using deleted elements map ...this.scene.getElementsIncludingDeleted().map((_element) => { if (_element.id === element.id && isTextElement(_element)) { return updateTextElement( _element, - getContainerElement( - _element, - this.scene.getElementsMapIncludingDeleted(), - ), + getContainerElement(_element, elementsMap), + elementsMap, { text, isDeleted, @@ -4238,7 +4241,7 @@ class App extends React.Component { onChange: withBatchedUpdates((text) => { updateElement(text, text, false); if (isNonDeletedElement(element)) { - updateBoundElements(element); + updateBoundElements(element, elementsMap); } }), onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => { @@ -4377,6 +4380,7 @@ class App extends React.Component { !(isTextElement(element) && element.containerId)), ); + const elementsMap = this.scene.getNonDeletedElementsMap(); return getElementsAtPosition(elements, (element) => hitTest( element, @@ -4384,7 +4388,7 @@ class App extends React.Component { this.frameNameBoundsCache, x, y, - this.scene.getNonDeletedElementsMap(), + elementsMap, ), ).filter((element) => { // hitting a frame's element from outside the frame is not considered a hit @@ -4392,7 +4396,7 @@ class App extends React.Component { return containingFrame && this.state.frameRendering.enabled && this.state.frameRendering.clip - ? isCursorInFrame({ x, y }, containingFrame) + ? isCursorInFrame({ x, y }, containingFrame, elementsMap) : true; }); } @@ -4637,6 +4641,7 @@ class App extends React.Component { this.state, sceneX, sceneY, + this.scene.getNonDeletedElementsMap(), ); if (container) { @@ -4648,6 +4653,7 @@ class App extends React.Component { this.state, this.frameNameBoundsCache, [sceneX, sceneY], + this.scene.getNonDeletedElementsMap(), ) ) { const midPoint = getContainerCenter( @@ -4688,6 +4694,7 @@ class App extends React.Component { index <= hitElementIndex && isPointHittingLink( element, + this.scene.getNonDeletedElementsMap(), this.state, [scenePointer.x, scenePointer.y], this.device.editor.isMobile, @@ -4718,8 +4725,10 @@ class App extends React.Component { this.lastPointerDownEvent!, this.state, ); + const elementsMap = this.scene.getNonDeletedElementsMap(); const lastPointerDownHittingLinkIcon = isPointHittingLink( this.hitLinkElement, + elementsMap, this.state, [lastPointerDownCoords.x, lastPointerDownCoords.y], this.device.editor.isMobile, @@ -4730,6 +4739,7 @@ class App extends React.Component { ); const lastPointerUpHittingLinkIcon = isPointHittingLink( this.hitLinkElement, + elementsMap, this.state, [lastPointerUpCoords.x, lastPointerUpCoords.y], this.device.editor.isMobile, @@ -4766,10 +4776,11 @@ class App extends React.Component { x: number; y: number; }) => { + const elementsMap = this.scene.getNonDeletedElementsMap(); const frames = this.scene .getNonDeletedFramesLikes() .filter((frame): frame is ExcalidrawFrameLikeElement => - isCursorInFrame(sceneCoords, frame), + isCursorInFrame(sceneCoords, frame, elementsMap), ); return frames.length ? frames[frames.length - 1] : null; @@ -4873,6 +4884,7 @@ class App extends React.Component { y: scenePointerY, }, event, + this.scene.getNonDeletedElementsMap(), ); this.setState((prevState) => { @@ -4912,6 +4924,7 @@ class App extends React.Component { scenePointerX, scenePointerY, this.state, + this.scene.getNonDeletedElementsMap(), ); if ( @@ -5062,6 +5075,7 @@ class App extends React.Component { scenePointerY, this.state.zoom, event.pointerType, + this.scene.getNonDeletedElementsMap(), ); if ( elementWithTransformHandleType && @@ -5109,7 +5123,11 @@ class App extends React.Component { !this.state.selectedElementIds[this.hitLinkElement.id] ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); - showHyperlinkTooltip(this.hitLinkElement, this.state); + showHyperlinkTooltip( + this.hitLinkElement, + this.state, + this.scene.getNonDeletedElementsMap(), + ); } else { hideHyperlinkToolip(); if ( @@ -5305,10 +5323,12 @@ class App extends React.Component { this.state, this.frameNameBoundsCache, [scenePointerX, scenePointerY], + elementsMap, ) ) { hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor( element, + elementsMap, this.state.zoom, scenePointerX, scenePointerY, @@ -5738,10 +5758,12 @@ class App extends React.Component { if ( clicklength < 300 && isIframeLikeElement(this.hitLinkElement) && - !isPointHittingLinkIcon(this.hitLinkElement, this.state, [ - scenePointer.x, - scenePointer.y, - ]) + !isPointHittingLinkIcon( + this.hitLinkElement, + this.scene.getNonDeletedElementsMap(), + this.state, + [scenePointer.x, scenePointer.y], + ) ) { this.handleEmbeddableCenterClick(this.hitLinkElement); } else { @@ -6039,7 +6061,9 @@ class App extends React.Component { ): boolean => { if (this.state.activeTool.type === "selection") { const elements = this.scene.getNonDeletedElements(); + const elementsMap = this.scene.getNonDeletedElementsMap(); const selectedElements = this.scene.getSelectedElements(this.state); + if (selectedElements.length === 1 && !this.state.editingLinearElement) { const elementWithTransformHandleType = getElementWithTransformHandleType( @@ -6049,6 +6073,7 @@ class App extends React.Component { pointerDownState.origin.y, this.state.zoom, event.pointerType, + this.scene.getNonDeletedElementsMap(), ); if (elementWithTransformHandleType != null) { this.setState({ @@ -6072,6 +6097,7 @@ class App extends React.Component { getResizeOffsetXY( pointerDownState.resize.handleType, selectedElements, + elementsMap, pointerDownState.origin.x, pointerDownState.origin.y, ), @@ -6352,6 +6378,7 @@ class App extends React.Component { this.state, sceneX, sceneY, + this.scene.getNonDeletedElementsMap(), ); if (hasBoundTextElement(element)) { @@ -6846,6 +6873,7 @@ class App extends React.Component { this.scene.getNonDeletedElements(), selectedElements, this.state, + this.scene.getNonDeletedElementsMap(), ), ); } @@ -6869,6 +6897,7 @@ class App extends React.Component { this.scene.getNonDeletedElements(), selectedElements, this.state, + this.scene.getNonDeletedElementsMap(), ), ); } @@ -6985,6 +7014,7 @@ class App extends React.Component { pointerCoords, this.state, !event[KEYS.CTRL_OR_CMD], + this.scene.getNonDeletedElementsMap(), ); if (!ret) { return; @@ -7143,10 +7173,11 @@ class App extends React.Component { this.maybeCacheReferenceSnapPoints(event, selectedElements); const { snapOffset, snapLines } = snapDraggedElements( - getSelectedElements(originalElements, this.state), + originalElements, dragOffset, this.state, event, + this.scene.getNonDeletedElementsMap(), ); this.setState({ snapLines }); @@ -7330,6 +7361,7 @@ class App extends React.Component { event, this.state, this.setState.bind(this), + this.scene.getNonDeletedElementsMap(), ); // regular box-select } else { @@ -7360,6 +7392,7 @@ class App extends React.Component { const elementsWithinSelection = getElementsWithinSelection( elements, draggingElement, + this.scene.getNonDeletedElementsMap(), ); this.setState((prevState) => { @@ -7491,7 +7524,7 @@ class App extends React.Component { this.setState({ selectedElementsAreBeingDragged: false, }); - + const elementsMap = this.scene.getNonDeletedElementsMap(); // Handle end of dragging a point of a linear element, might close a loop // and sets binding element if (this.state.editingLinearElement) { @@ -7506,6 +7539,7 @@ class App extends React.Component { childEvent, this.state.editingLinearElement, this.state, + elementsMap, ); if (editingLinearElement !== this.state.editingLinearElement) { this.setState({ @@ -7529,6 +7563,7 @@ class App extends React.Component { childEvent, this.state.selectedLinearElement, this.state, + elementsMap, ); const { startBindingElement, endBindingElement } = @@ -7539,6 +7574,7 @@ class App extends React.Component { element, startBindingElement, endBindingElement, + elementsMap, ); } @@ -7678,6 +7714,7 @@ class App extends React.Component { this.state, this.scene, pointerCoords, + elementsMap, ); } this.setState({ suggestedBindings: [], startBoundElement: null }); @@ -7748,7 +7785,13 @@ class App extends React.Component { const frame = getContainingFrame(linearElement); if (frame && linearElement) { - if (!elementOverlapsWithFrame(linearElement, frame)) { + if ( + !elementOverlapsWithFrame( + linearElement, + frame, + this.scene.getNonDeletedElementsMap(), + ) + ) { // remove the linear element from all groups // before removing it from the frame as well mutateElement(linearElement, { @@ -7859,6 +7902,7 @@ class App extends React.Component { const elementsInsideFrame = getElementsInNewFrame( this.scene.getElementsIncludingDeleted(), draggingElement, + this.scene.getNonDeletedElementsMap(), ); this.scene.replaceAllElements( @@ -7909,6 +7953,7 @@ class App extends React.Component { this.scene.getElementsIncludingDeleted(), frame, this.state, + elementsMap, ), frame, this, @@ -8189,7 +8234,10 @@ class App extends React.Component { if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { (isBindingEnabled(this.state) ? bindOrUnbindSelectedElements - : unbindLinearElements)(this.scene.getSelectedElements(this.state)); + : unbindLinearElements)( + this.scene.getSelectedElements(this.state), + elementsMap, + ); } if (activeTool.type === "laser") { @@ -8719,7 +8767,10 @@ class App extends React.Component { if (selectedElements.length > 50) { return; } - const suggestedBindings = getEligibleElementsForBinding(selectedElements); + const suggestedBindings = getEligibleElementsForBinding( + selectedElements, + this.scene.getNonDeletedElementsMap(), + ); this.setState({ suggestedBindings }); } @@ -9058,6 +9109,7 @@ class App extends React.Component { x: gridX - pointerDownState.originInGrid.x, y: gridY - pointerDownState.originInGrid.y, }, + this.scene.getNonDeletedElementsMap(), ); gridX += snapOffset.x; @@ -9096,6 +9148,7 @@ class App extends React.Component { this.scene.getNonDeletedElements(), draggingElement as ExcalidrawFrameLikeElement, this.state, + this.scene.getNonDeletedElementsMap(), ), }); } @@ -9215,6 +9268,7 @@ class App extends React.Component { this.scene.getNonDeletedElements(), frame, this.state, + this.scene.getNonDeletedElementsMap(), ).forEach((element) => elementsToHighlight.add(element)); }); diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 12e7f1af1b..022457f011 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -462,6 +462,7 @@ export const restoreElements = ( refreshTextDimensions( element, getContainerElement(element, restoredElementsMap), + restoredElementsMap, ), ); } diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts index 8ce842300c..8d5b63a19d 100644 --- a/packages/excalidraw/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -222,7 +222,7 @@ const bindTextToContainer = ( }), }); - redrawTextBoundingBox(textElement, container); + redrawTextBoundingBox(textElement, container, elementsMap); return [container, textElement] as const; }; @@ -231,6 +231,7 @@ const bindLinearElementToElement = ( start: ValidLinearElement["start"], end: ValidLinearElement["end"], elementStore: ElementStore, + elementsMap: ElementsMap, ): { linearElement: ExcalidrawLinearElement; startBoundElement?: ExcalidrawElement; @@ -316,6 +317,7 @@ const bindLinearElementToElement = ( linearElement, startBoundElement as ExcalidrawBindableElement, "start", + elementsMap, ); } } @@ -390,6 +392,7 @@ const bindLinearElementToElement = ( linearElement, endBoundElement as ExcalidrawBindableElement, "end", + elementsMap, ); } } @@ -612,6 +615,7 @@ export const convertToExcalidrawElements = ( } } + const elementsMap = arrayToMap(elementStore.getElements()); // Add labels and arrow bindings for (const [id, element] of elementsWithIds) { const excalidrawElement = elementStore.getElement(id)!; @@ -625,7 +629,7 @@ export const convertToExcalidrawElements = ( let [container, text] = bindTextToContainer( excalidrawElement, element?.label, - arrayToMap(elementStore.getElements()), + elementsMap, ); elementStore.add(container); elementStore.add(text); @@ -653,6 +657,7 @@ export const convertToExcalidrawElements = ( originalStart, originalEnd, elementStore, + elementsMap, ); container = linearElement; elementStore.add(linearElement); @@ -677,6 +682,7 @@ export const convertToExcalidrawElements = ( start, end, elementStore, + elementsMap, ); elementStore.add(linearElement); diff --git a/packages/excalidraw/element/ElementCanvasButtons.tsx b/packages/excalidraw/element/ElementCanvasButtons.tsx index 99d9d55e1b..0fc7621fda 100644 --- a/packages/excalidraw/element/ElementCanvasButtons.tsx +++ b/packages/excalidraw/element/ElementCanvasButtons.tsx @@ -1,6 +1,6 @@ import { AppState } from "../types"; import { sceneCoordsToViewportCoords } from "../utils"; -import { NonDeletedExcalidrawElement } from "./types"; +import { ElementsMap, NonDeletedExcalidrawElement } from "./types"; import { getElementAbsoluteCoords } from "."; import { useExcalidrawAppState } from "../components/App"; @@ -11,8 +11,9 @@ const CONTAINER_PADDING = 5; const getContainerCoords = ( element: NonDeletedExcalidrawElement, appState: AppState, + elementsMap: ElementsMap, ) => { - const [x1, y1] = getElementAbsoluteCoords(element); + const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( { sceneX: x1 + element.width, sceneY: y1 }, appState, @@ -25,9 +26,11 @@ const getContainerCoords = ( export const ElementCanvasButtons = ({ children, element, + elementsMap, }: { children: React.ReactNode; element: NonDeletedExcalidrawElement; + elementsMap: ElementsMap; }) => { const appState = useExcalidrawAppState(); @@ -42,7 +45,7 @@ export const ElementCanvasButtons = ({ return null; } - const { x, y } = getContainerCoords(element, appState); + const { x, y } = getContainerCoords(element, appState, elementsMap); return (
["setState"]; onLinkOpen: ExcalidrawProps["onLinkOpen"]; setToast: ( @@ -182,7 +185,7 @@ export const Hyperlink = ({ if (timeoutId) { clearTimeout(timeoutId); } - const shouldHide = shouldHideLinkPopup(element, appState, [ + const shouldHide = shouldHideLinkPopup(element, elementsMap, appState, [ event.clientX, event.clientY, ]) as boolean; @@ -199,7 +202,7 @@ export const Hyperlink = ({ clearTimeout(timeoutId); } }; - }, [appState, element, isEditing, setAppState]); + }, [appState, element, isEditing, setAppState, elementsMap]); const handleRemove = useCallback(() => { trackEvent("hyperlink", "delete"); @@ -214,7 +217,7 @@ export const Hyperlink = ({ trackEvent("hyperlink", "edit", "popup-ui"); setAppState({ showHyperlinkPopup: "editor" }); }; - const { x, y } = getCoordsForPopover(element, appState); + const { x, y } = getCoordsForPopover(element, appState, elementsMap); if ( appState.contextMenu || appState.draggingElement || @@ -324,8 +327,9 @@ export const Hyperlink = ({ const getCoordsForPopover = ( element: NonDeletedExcalidrawElement, appState: AppState, + elementsMap: ElementsMap, ) => { - const [x1, y1] = getElementAbsoluteCoords(element); + const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( { sceneX: x1 + element.width / 2, sceneY: y1 }, appState, @@ -430,11 +434,12 @@ export const getLinkHandleFromCoords = ( export const isPointHittingLinkIcon = ( element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, appState: AppState, [x, y]: Point, ) => { const threshold = 4 / appState.zoom.value; - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords( [x1, y1, x2, y2], element.angle, @@ -450,6 +455,7 @@ export const isPointHittingLinkIcon = ( export const isPointHittingLink = ( element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, appState: AppState, [x, y]: Point, isMobile: boolean, @@ -461,23 +467,30 @@ export const isPointHittingLink = ( if ( !isMobile && appState.viewModeEnabled && - isPointHittingElementBoundingBox(element, [x, y], threshold, null) + isPointHittingElementBoundingBox( + element, + elementsMap, + [x, y], + threshold, + null, + ) ) { return true; } - return isPointHittingLinkIcon(element, appState, [x, y]); + return isPointHittingLinkIcon(element, elementsMap, appState, [x, y]); }; let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null; export const showHyperlinkTooltip = ( element: NonDeletedExcalidrawElement, appState: AppState, + elementsMap: ElementsMap, ) => { if (HYPERLINK_TOOLTIP_TIMEOUT_ID) { clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID); } HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout( - () => renderTooltip(element, appState), + () => renderTooltip(element, appState, elementsMap), HYPERLINK_TOOLTIP_DELAY, ); }; @@ -485,6 +498,7 @@ export const showHyperlinkTooltip = ( const renderTooltip = ( element: NonDeletedExcalidrawElement, appState: AppState, + elementsMap: ElementsMap, ) => { if (!element.link) { return; @@ -496,7 +510,7 @@ const renderTooltip = ( tooltipDiv.style.maxWidth = "20rem"; tooltipDiv.textContent = element.link; - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords( [x1, y1, x2, y2], @@ -535,6 +549,7 @@ export const hideHyperlinkToolip = () => { export const shouldHideLinkPopup = ( element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, appState: AppState, [clientX, clientY]: Point, ): Boolean => { @@ -546,11 +561,17 @@ export const shouldHideLinkPopup = ( const threshold = 15 / appState.zoom.value; // hitbox to prevent hiding when hovered in element bounding box if ( - isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold, null) + isPointHittingElementBoundingBox( + element, + elementsMap, + [sceneX, sceneY], + threshold, + null, + ) ) { return false; } - const [x1, y1, x2] = getElementAbsoluteCoords(element); + const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap); // hit box to prevent hiding when hovered in the vertical area between element and popover if ( sceneX >= x1 && @@ -561,7 +582,11 @@ export const shouldHideLinkPopup = ( return false; } // hit box to prevent hiding when hovered around popover within threshold - const { x: popoverX, y: popoverY } = getCoordsForPopover(element, appState); + const { x: popoverX, y: popoverY } = getCoordsForPopover( + element, + appState, + elementsMap, + ); if ( clientX >= popoverX - threshold && diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index 66d29f3f63..be766e33ff 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -5,6 +5,7 @@ import { NonDeletedExcalidrawElement, PointBinding, ExcalidrawElement, + ElementsMap, } from "./types"; import { getElementAtPosition } from "../scene"; import { AppState } from "../types"; @@ -66,6 +67,7 @@ export const bindOrUnbindLinearElement = ( linearElement: NonDeleted, startBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep", + elementsMap: ElementsMap, ): void => { const boundToElementIds: Set = new Set(); const unboundFromElementIds: Set = new Set(); @@ -76,6 +78,7 @@ export const bindOrUnbindLinearElement = ( "start", boundToElementIds, unboundFromElementIds, + elementsMap, ); bindOrUnbindLinearElementEdge( linearElement, @@ -84,6 +87,7 @@ export const bindOrUnbindLinearElement = ( "end", boundToElementIds, unboundFromElementIds, + elementsMap, ); const onlyUnbound = Array.from(unboundFromElementIds).filter( @@ -111,6 +115,7 @@ const bindOrUnbindLinearElementEdge = ( boundToElementIds: Set, // Is mutated unboundFromElementIds: Set, + elementsMap: ElementsMap, ): void => { if (bindableElement !== "keep") { if (bindableElement != null) { @@ -127,7 +132,12 @@ const bindOrUnbindLinearElementEdge = ( : startOrEnd === "start" || otherEdgeBindableElement.id !== bindableElement.id) ) { - bindLinearElement(linearElement, bindableElement, startOrEnd); + bindLinearElement( + linearElement, + bindableElement, + startOrEnd, + elementsMap, + ); boundToElementIds.add(bindableElement.id); } } else { @@ -140,23 +150,34 @@ const bindOrUnbindLinearElementEdge = ( }; export const bindOrUnbindSelectedElements = ( - elements: NonDeleted[], + selectedElements: NonDeleted[], + elementsMap: ElementsMap, ): void => { - elements.forEach((element) => { - if (isBindingElement(element)) { + selectedElements.forEach((selectedElement) => { + if (isBindingElement(selectedElement)) { bindOrUnbindLinearElement( - element, - getElligibleElementForBindingElement(element, "start"), - getElligibleElementForBindingElement(element, "end"), + selectedElement, + getElligibleElementForBindingElement( + selectedElement, + "start", + elementsMap, + ), + getElligibleElementForBindingElement( + selectedElement, + "end", + elementsMap, + ), + elementsMap, ); - } else if (isBindableElement(element)) { - maybeBindBindableElement(element); + } else if (isBindableElement(selectedElement)) { + maybeBindBindableElement(selectedElement, elementsMap); } }); }; const maybeBindBindableElement = ( bindableElement: NonDeleted, + elementsMap: ElementsMap, ): void => { getElligibleElementsForBindableElementAndWhere(bindableElement).forEach( ([linearElement, where]) => @@ -164,6 +185,7 @@ const maybeBindBindableElement = ( linearElement, where === "end" ? "keep" : bindableElement, where === "start" ? "keep" : bindableElement, + elementsMap, ), ); }; @@ -173,9 +195,15 @@ export const maybeBindLinearElement = ( appState: AppState, scene: Scene, pointerCoords: { x: number; y: number }, + elementsMap: ElementsMap, ): void => { if (appState.startBoundElement != null) { - bindLinearElement(linearElement, appState.startBoundElement, "start"); + bindLinearElement( + linearElement, + appState.startBoundElement, + "start", + elementsMap, + ); } const hoveredElement = getHoveredElementForBinding(pointerCoords, scene); if ( @@ -186,7 +214,7 @@ export const maybeBindLinearElement = ( "end", ) ) { - bindLinearElement(linearElement, hoveredElement, "end"); + bindLinearElement(linearElement, hoveredElement, "end", elementsMap); } }; @@ -194,11 +222,17 @@ export const bindLinearElement = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", + elementsMap: ElementsMap, ): void => { mutateElement(linearElement, { [startOrEnd === "start" ? "startBinding" : "endBinding"]: { elementId: hoveredElement.id, - ...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd), + ...calculateFocusAndGap( + linearElement, + hoveredElement, + startOrEnd, + elementsMap, + ), } as PointBinding, }); @@ -240,10 +274,11 @@ export const isLinearElementSimpleAndAlreadyBound = ( export const unbindLinearElements = ( elements: NonDeleted[], + elementsMap: ElementsMap, ): void => { elements.forEach((element) => { if (isBindingElement(element)) { - bindOrUnbindLinearElement(element, null, null); + bindOrUnbindLinearElement(element, null, null, elementsMap); } }); }; @@ -272,7 +307,11 @@ export const getHoveredElementForBinding = ( scene.getNonDeletedElements(), (element) => isBindableElement(element, false) && - bindingBorderTest(element, pointerCoords), + bindingBorderTest( + element, + pointerCoords, + scene.getNonDeletedElementsMap(), + ), ); return hoveredElement as NonDeleted | null; }; @@ -281,21 +320,33 @@ const calculateFocusAndGap = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", + elementsMap: ElementsMap, ): { focus: number; gap: number } => { const direction = startOrEnd === "start" ? -1 : 1; const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1; const adjacentPointIndex = edgePointIndex - direction; + const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( linearElement, edgePointIndex, + elementsMap, ); const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( linearElement, adjacentPointIndex, + elementsMap, ); return { - focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), - gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)), + focus: determineFocusDistance( + hoveredElement, + adjacentPoint, + edgePoint, + elementsMap, + ), + gap: Math.max( + 1, + distanceToBindableElement(hoveredElement, edgePoint, elementsMap), + ), }; }; @@ -306,6 +357,8 @@ const calculateFocusAndGap = ( // in explicitly. export const updateBoundElements = ( changedElement: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, + options?: { simultaneouslyUpdated?: readonly ExcalidrawElement[]; newSize?: { width: number; height: number }; @@ -355,12 +408,14 @@ export const updateBoundElements = ( "start", startBinding, changedElement as ExcalidrawBindableElement, + elementsMap, ); updateBoundPoint( element, "end", endBinding, changedElement as ExcalidrawBindableElement, + elementsMap, ); const boundText = getBoundTextElement( element, @@ -393,6 +448,7 @@ const updateBoundPoint = ( startOrEnd: "start" | "end", binding: PointBinding | null | undefined, changedElement: ExcalidrawBindableElement, + elementsMap: ElementsMap, ): void => { if ( binding == null || @@ -414,11 +470,13 @@ const updateBoundPoint = ( const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( linearElement, adjacentPointIndex, + elementsMap, ); const focusPointAbsolute = determineFocusPoint( bindingElement, binding.focus, adjacentPoint, + elementsMap, ); let newEdgePoint; // The linear element was not originally pointing inside the bound shape, @@ -431,6 +489,7 @@ const updateBoundPoint = ( adjacentPoint, focusPointAbsolute, binding.gap, + elementsMap, ); if (intersections.length === 0) { // This should never happen, since focusPoint should always be @@ -449,6 +508,7 @@ const updateBoundPoint = ( point: LinearElementEditor.pointFromAbsoluteCoords( linearElement, newEdgePoint, + elementsMap, ), }, ], @@ -480,12 +540,14 @@ const maybeCalculateNewGapWhenScaling = ( // TODO: this is a bottleneck, optimise export const getEligibleElementsForBinding = ( elements: NonDeleted[], + elementsMap: ElementsMap, ): SuggestedBinding[] => { const includedElementIds = new Set(elements.map(({ id }) => id)); return elements.flatMap((element) => isBindingElement(element, false) ? (getElligibleElementsForBindingElement( element as NonDeleted, + elementsMap, ).filter( (element) => !includedElementIds.has(element.id), ) as SuggestedBinding[]) @@ -499,10 +561,11 @@ export const getEligibleElementsForBinding = ( const getElligibleElementsForBindingElement = ( linearElement: NonDeleted, + elementsMap: ElementsMap, ): NonDeleted[] => { return [ - getElligibleElementForBindingElement(linearElement, "start"), - getElligibleElementForBindingElement(linearElement, "end"), + getElligibleElementForBindingElement(linearElement, "start", elementsMap), + getElligibleElementForBindingElement(linearElement, "end", elementsMap), ].filter( (element): element is NonDeleted => element != null, @@ -512,9 +575,10 @@ const getElligibleElementsForBindingElement = ( const getElligibleElementForBindingElement = ( linearElement: NonDeleted, startOrEnd: "start" | "end", + elementsMap: ElementsMap, ): NonDeleted | null => { return getHoveredElementForBinding( - getLinearElementEdgeCoors(linearElement, startOrEnd), + getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap), Scene.getScene(linearElement)!, ); }; @@ -522,17 +586,23 @@ const getElligibleElementForBindingElement = ( const getLinearElementEdgeCoors = ( linearElement: NonDeleted, startOrEnd: "start" | "end", + elementsMap: ElementsMap, ): { x: number; y: number } => { const index = startOrEnd === "start" ? 0 : -1; return tupleToCoors( - LinearElementEditor.getPointAtIndexGlobalCoordinates(linearElement, index), + LinearElementEditor.getPointAtIndexGlobalCoordinates( + linearElement, + index, + elementsMap, + ), ); }; const getElligibleElementsForBindableElementAndWhere = ( bindableElement: NonDeleted, ): SuggestedPointBinding[] => { - return Scene.getScene(bindableElement)! + const scene = Scene.getScene(bindableElement)!; + return scene .getNonDeletedElements() .map((element) => { if (!isBindingElement(element, false)) { @@ -542,11 +612,13 @@ const getElligibleElementsForBindableElementAndWhere = ( element, "start", bindableElement, + scene.getNonDeletedElementsMap(), ); const canBindEnd = isLinearElementEligibleForNewBindingByBindable( element, "end", bindableElement, + scene.getNonDeletedElementsMap(), ); if (!canBindStart && !canBindEnd) { return null; @@ -564,6 +636,7 @@ const isLinearElementEligibleForNewBindingByBindable = ( linearElement: NonDeleted, startOrEnd: "start" | "end", bindableElement: NonDeleted, + elementsMap: ElementsMap, ): boolean => { const existingBinding = linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"]; @@ -576,7 +649,8 @@ const isLinearElementEligibleForNewBindingByBindable = ( ) && bindingBorderTest( bindableElement, - getLinearElementEdgeCoors(linearElement, startOrEnd), + getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap), + elementsMap, ) ); }; diff --git a/packages/excalidraw/element/bounds.test.ts b/packages/excalidraw/element/bounds.test.ts index 850c506541..253137b074 100644 --- a/packages/excalidraw/element/bounds.test.ts +++ b/packages/excalidraw/element/bounds.test.ts @@ -1,4 +1,5 @@ import { ROUNDNESS } from "../constants"; +import { arrayToMap } from "../utils"; import { getElementAbsoluteCoords, getElementBounds } from "./bounds"; import { ExcalidrawElement, ExcalidrawLinearElement } from "./types"; @@ -35,26 +36,26 @@ const _ce = ({ describe("getElementAbsoluteCoords", () => { it("test x1 coordinate", () => { - const [x1] = getElementAbsoluteCoords(_ce({ x: 10, y: 0, w: 10, h: 0 })); + const element = _ce({ x: 10, y: 20, w: 10, h: 0 }); + const [x1] = getElementAbsoluteCoords(element, arrayToMap([element])); expect(x1).toEqual(10); }); it("test x2 coordinate", () => { - const [, , x2] = getElementAbsoluteCoords( - _ce({ x: 10, y: 0, w: 10, h: 0 }), - ); + const element = _ce({ x: 10, y: 20, w: 10, h: 0 }); + const [, , x2] = getElementAbsoluteCoords(element, arrayToMap([element])); expect(x2).toEqual(20); }); it("test y1 coordinate", () => { - const [, y1] = getElementAbsoluteCoords(_ce({ x: 0, y: 10, w: 0, h: 10 })); + const element = _ce({ x: 0, y: 10, w: 0, h: 10 }); + const [, y1] = getElementAbsoluteCoords(element, arrayToMap([element])); expect(y1).toEqual(10); }); it("test y2 coordinate", () => { - const [, , , y2] = getElementAbsoluteCoords( - _ce({ x: 0, y: 10, w: 0, h: 10 }), - ); + const element = _ce({ x: 0, y: 10, w: 0, h: 10 }); + const [, , , y2] = getElementAbsoluteCoords(element, arrayToMap([element])); expect(y2).toEqual(20); }); }); diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index f892089f75..7eb7fa48a8 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -102,8 +102,10 @@ export class ElementBounds { ): Bounds { let bounds: Bounds; - const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); - + const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + ); if (isFreeDrawElement(element)) { const [minX, minY, maxX, maxY] = getBoundsFromPoints( element.points.map(([x, y]) => @@ -159,10 +161,9 @@ export class ElementBounds { // This set of functions retrieves the absolute position of the 4 points. export const getElementAbsoluteCoords = ( element: ExcalidrawElement, + elementsMap: ElementsMap, 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)) { @@ -179,6 +180,7 @@ export const getElementAbsoluteCoords = ( const coords = LinearElementEditor.getBoundTextElementPosition( container, element as ExcalidrawTextElementWithContainer, + elementsMap, ); return [ coords.x, @@ -207,8 +209,12 @@ export const getElementAbsoluteCoords = ( */ export const getElementLineSegments = ( element: ExcalidrawElement, + elementsMap: ElementsMap, ): [Point, Point][] => { - const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + ); const center: Point = [cx, cy]; @@ -703,6 +709,7 @@ const getLinearElementRotatedBounds = ( if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, + elementsMap, [x, y, x, y], boundTextElement, ); @@ -727,6 +734,7 @@ const getLinearElementRotatedBounds = ( if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, + elementsMap, coords, boundTextElement, ); diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index b8c07e3ab0..ff5a139de0 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -91,6 +91,7 @@ export const hitTest = ( ) { return isPointHittingElementBoundingBox( element, + elementsMap, point, threshold, frameNameBoundsCache, @@ -116,6 +117,7 @@ export const hitTest = ( appState, frameNameBoundsCache, point, + elementsMap, ); }; @@ -145,9 +147,11 @@ export const isHittingElementBoundingBoxWithoutHittingElement = ( appState, frameNameBoundsCache, [x, y], + elementsMap, ) && isPointHittingElementBoundingBox( element, + elementsMap, [x, y], threshold, frameNameBoundsCache, @@ -160,6 +164,7 @@ export const isHittingElementNotConsideringBoundingBox = ( appState: AppState, frameNameBoundsCache: FrameNameBoundsCache | null, point: Point, + elementsMap: ElementsMap, ): boolean => { const threshold = 10 / appState.zoom.value; const check = isTextElement(element) @@ -169,6 +174,7 @@ export const isHittingElementNotConsideringBoundingBox = ( : isNearCheck; return hitTestPointAgainstElement({ element, + elementsMap, point, threshold, check, @@ -183,6 +189,7 @@ const isElementSelected = ( export const isPointHittingElementBoundingBox = ( element: NonDeleted, + elementsMap: ElementsMap, [x, y]: Point, threshold: number, frameNameBoundsCache: FrameNameBoundsCache | null, @@ -194,6 +201,7 @@ export const isPointHittingElementBoundingBox = ( if (isFrameLikeElement(element)) { return hitTestPointAgainstElement({ element, + elementsMap, point: [x, y], threshold, check: isInsideCheck, @@ -201,7 +209,7 @@ export const isPointHittingElementBoundingBox = ( }); } - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const elementCenterX = (x1 + x2) / 2; const elementCenterY = (y1 + y2) / 2; // reverse rotate to take element's angle into account. @@ -224,12 +232,14 @@ export const isPointHittingElementBoundingBox = ( export const bindingBorderTest = ( element: NonDeleted, { x, y }: { x: number; y: number }, + elementsMap: ElementsMap, ): boolean => { const threshold = maxBindingGap(element, element.width, element.height); const check = isOutsideCheck; const point: Point = [x, y]; return hitTestPointAgainstElement({ element, + elementsMap, point, threshold, check, @@ -251,6 +261,7 @@ export const maxBindingGap = ( type HitTestArgs = { element: NonDeletedExcalidrawElement; + elementsMap: ElementsMap; point: Point; threshold: number; check: (distance: number, threshold: number) => boolean; @@ -266,19 +277,28 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => { case "text": case "diamond": case "ellipse": - const distance = distanceToBindableElement(args.element, args.point); + const distance = distanceToBindableElement( + args.element, + args.point, + args.elementsMap, + ); return args.check(distance, args.threshold); case "freedraw": { if ( !args.check( - distanceToRectangle(args.element, args.point), + distanceToRectangle(args.element, args.point, args.elementsMap), args.threshold, ) ) { return false; } - return hitTestFreeDrawElement(args.element, args.point, args.threshold); + return hitTestFreeDrawElement( + args.element, + args.point, + args.threshold, + args.elementsMap, + ); } case "arrow": case "line": @@ -293,7 +313,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => { // check distance to frame element first if ( args.check( - distanceToBindableElement(args.element, args.point), + distanceToBindableElement(args.element, args.point, args.elementsMap), args.threshold, ) ) { @@ -316,6 +336,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => { export const distanceToBindableElement = ( element: ExcalidrawBindableElement, point: Point, + elementsMap: ElementsMap, ): number => { switch (element.type) { case "rectangle": @@ -325,11 +346,11 @@ export const distanceToBindableElement = ( case "embeddable": case "frame": case "magicframe": - return distanceToRectangle(element, point); + return distanceToRectangle(element, point, elementsMap); case "diamond": - return distanceToDiamond(element, point); + return distanceToDiamond(element, point, elementsMap); case "ellipse": - return distanceToEllipse(element, point); + return distanceToEllipse(element, point, elementsMap); } }; @@ -358,8 +379,13 @@ const distanceToRectangle = ( | ExcalidrawIframeLikeElement | ExcalidrawFrameLikeElement, point: Point, + elementsMap: ElementsMap, ): number => { - const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); + const [, pointRel, hwidth, hheight] = pointRelativeToElement( + element, + point, + elementsMap, + ); return Math.max( GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)), GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)), @@ -377,8 +403,13 @@ const distanceToRectangleBox = (box: RectangleBox, point: Point): number => { const distanceToDiamond = ( element: ExcalidrawDiamondElement, point: Point, + elementsMap: ElementsMap, ): number => { - const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); + const [, pointRel, hwidth, hheight] = pointRelativeToElement( + element, + point, + elementsMap, + ); const side = GALine.equation(hheight, hwidth, -hheight * hwidth); return GAPoint.distanceToLine(pointRel, side); }; @@ -386,16 +417,22 @@ const distanceToDiamond = ( const distanceToEllipse = ( element: ExcalidrawEllipseElement, point: Point, + elementsMap: ElementsMap, ): number => { - const [pointRel, tangent] = ellipseParamsForTest(element, point); + const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap); return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent); }; const ellipseParamsForTest = ( element: ExcalidrawEllipseElement, point: Point, + elementsMap: ElementsMap, ): [GA.Point, GA.Line] => { - const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); + const [, pointRel, hwidth, hheight] = pointRelativeToElement( + element, + point, + elementsMap, + ); const [px, py] = GAPoint.toTuple(pointRel); // We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)` @@ -440,6 +477,7 @@ const hitTestFreeDrawElement = ( element: ExcalidrawFreeDrawElement, point: Point, threshold: number, + elementsMap: ElementsMap, ): boolean => { // Check point-distance-to-line-segment for every segment in the // element's points (its input points, not its outline points). @@ -454,7 +492,10 @@ const hitTestFreeDrawElement = ( y = point[1] - element.y; } else { // Counter-rotate the point around center before testing - const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(element); + const [minX, minY, maxX, maxY] = getElementAbsoluteCoords( + element, + elementsMap, + ); const rotatedPoint = rotatePoint( point, [minX + (maxX - minX) / 2, minY + (maxY - minY) / 2], @@ -520,6 +561,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => { const [point, pointAbs, hwidth, hheight] = pointRelativeToElement( args.element, args.point, + args.elementsMap, ); const side1 = GALine.equation(0, 1, -hheight); const side2 = GALine.equation(1, 0, -hwidth); @@ -572,9 +614,10 @@ const hitTestLinear = (args: HitTestArgs): boolean => { const pointRelativeToElement = ( element: ExcalidrawElement, pointTuple: Point, + elementsMap: ElementsMap, ): [GA.Point, GA.Point, number, number] => { const point = GAPoint.from(pointTuple); - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const center = coordsCenter(x1, y1, x2, y2); // GA has angle orientation opposite to `rotate` const rotate = GATransform.rotation(center, element.angle); @@ -609,11 +652,12 @@ const pointRelativeToDivElement = ( // Returns point in absolute coordinates export const pointInAbsoluteCoords = ( element: ExcalidrawElement, + elementsMap: ElementsMap, // Point relative to the element position point: Point, ): Point => { const [x, y] = point; - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x2 - x1) / 2; const cy = (y2 - y1) / 2; const [rotatedX, rotatedY] = rotate(x, y, cx, cy, element.angle); @@ -622,8 +666,9 @@ export const pointInAbsoluteCoords = ( const relativizationToElementCenter = ( element: ExcalidrawElement, + elementsMap: ElementsMap, ): GA.Transform => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const center = coordsCenter(x1, y1, x2, y2); // GA has angle orientation opposite to `rotate` const rotate = GATransform.rotation(center, element.angle); @@ -649,12 +694,14 @@ const coordsCenter = ( // of the element. export const determineFocusDistance = ( element: ExcalidrawBindableElement, + // Point on the line, in absolute coordinates a: Point, // Another point on the line, in absolute coordinates (closer to element) b: Point, + elementsMap: ElementsMap, ): number => { - const relateToCenter = relativizationToElementCenter(element); + const relateToCenter = relativizationToElementCenter(element, elementsMap); const aRel = GATransform.apply(relateToCenter, GAPoint.from(a)); const bRel = GATransform.apply(relateToCenter, GAPoint.from(b)); const line = GALine.through(aRel, bRel); @@ -693,13 +740,14 @@ export const determineFocusPoint = ( // returned focusPoint focus: number, adjecentPoint: Point, + elementsMap: ElementsMap, ): Point => { if (focus === 0) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const center = coordsCenter(x1, y1, x2, y2); return GAPoint.toTuple(center); } - const relateToCenter = relativizationToElementCenter(element); + const relateToCenter = relativizationToElementCenter(element, elementsMap); const adjecentPointRel = GATransform.apply( relateToCenter, GAPoint.from(adjecentPoint), @@ -728,14 +776,16 @@ export const determineFocusPoint = ( // and the `element`, in ascending order of distance from `a`. export const intersectElementWithLine = ( element: ExcalidrawBindableElement, + // Point on the line, in absolute coordinates a: Point, // Another point on the line, in absolute coordinates b: Point, // If given, the element is inflated by this value gap: number = 0, + elementsMap: ElementsMap, ): Point[] => { - const relateToCenter = relativizationToElementCenter(element); + const relateToCenter = relativizationToElementCenter(element, elementsMap); const aRel = GATransform.apply(relateToCenter, GAPoint.from(a)); const bRel = GATransform.apply(relateToCenter, GAPoint.from(b)); const line = GALine.through(aRel, bRel); diff --git a/packages/excalidraw/element/dragElements.ts b/packages/excalidraw/element/dragElements.ts index 0144f55a40..5121f52bd5 100644 --- a/packages/excalidraw/element/dragElements.ts +++ b/packages/excalidraw/element/dragElements.ts @@ -65,7 +65,7 @@ export const dragSelectedElements = ( updateElementCoords(pointerDownState, textElement, adjustedOffset); } } - updateBoundElements(element, { + updateBoundElements(element, scene.getElementsMapIncludingDeleted(), { simultaneouslyUpdated: Array.from(elementsToUpdate), }); }); diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index 5c3c6acaa2..85483b3d72 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -135,6 +135,7 @@ export class LinearElementEditor { event: PointerEvent, appState: AppState, setState: React.Component["setState"], + elementsMap: ElementsMap, ) { if ( !appState.editingLinearElement || @@ -151,10 +152,12 @@ export class LinearElementEditor { } const [selectionX1, selectionY1, selectionX2, selectionY2] = - getElementAbsoluteCoords(appState.draggingElement); + getElementAbsoluteCoords(appState.draggingElement, elementsMap); - const pointsSceneCoords = - LinearElementEditor.getPointsGlobalCoordinates(element); + const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates( + element, + elementsMap, + ); const nextSelectedPoints = pointsSceneCoords.reduce( (acc: number[], point, index) => { @@ -222,6 +225,7 @@ export class LinearElementEditor { const [width, height] = LinearElementEditor._getShiftLockedDelta( element, + elementsMap, referencePoint, [scenePointerX, scenePointerY], event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, @@ -239,6 +243,7 @@ export class LinearElementEditor { } else { const newDraggingPointPosition = LinearElementEditor.createPointAt( element, + elementsMap, scenePointerX - linearElementEditor.pointerOffset.x, scenePointerY - linearElementEditor.pointerOffset.y, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, @@ -255,6 +260,7 @@ export class LinearElementEditor { linearElementEditor.pointerDownState.lastClickedPoint ? LinearElementEditor.createPointAt( element, + elementsMap, scenePointerX - linearElementEditor.pointerOffset.x, scenePointerY - linearElementEditor.pointerOffset.y, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, @@ -290,6 +296,7 @@ export class LinearElementEditor { LinearElementEditor.getPointGlobalCoordinates( element, element.points[0], + elementsMap, ), ), ); @@ -303,6 +310,7 @@ export class LinearElementEditor { LinearElementEditor.getPointGlobalCoordinates( element, element.points[lastSelectedIndex], + elementsMap, ), ), ); @@ -323,6 +331,7 @@ export class LinearElementEditor { event: PointerEvent, editingLinearElement: LinearElementEditor, appState: AppState, + elementsMap: ElementsMap, ): LinearElementEditor { const { elementId, selectedPointsIndices, isDragging, pointerDownState } = editingLinearElement; @@ -364,6 +373,7 @@ export class LinearElementEditor { LinearElementEditor.getPointAtIndexGlobalCoordinates( element, selectedPoint!, + elementsMap, ), ), Scene.getScene(element)!, @@ -425,15 +435,23 @@ export class LinearElementEditor { ) { return editorMidPointsCache.points; } - LinearElementEditor.updateEditorMidPointsCache(element, appState); + LinearElementEditor.updateEditorMidPointsCache( + element, + elementsMap, + appState, + ); return editorMidPointsCache.points!; }; static updateEditorMidPointsCache = ( element: NonDeleted, + elementsMap: ElementsMap, appState: InteractiveCanvasAppState, ) => { - const points = LinearElementEditor.getPointsGlobalCoordinates(element); + const points = LinearElementEditor.getPointsGlobalCoordinates( + element, + elementsMap, + ); let index = 0; const midpoints: (Point | null)[] = []; @@ -455,6 +473,7 @@ export class LinearElementEditor { points[index], points[index + 1], index + 1, + elementsMap, ); midpoints.push(segmentMidPoint); index++; @@ -477,6 +496,7 @@ export class LinearElementEditor { } const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( element, + elementsMap, appState.zoom, scenePointer.x, scenePointer.y, @@ -484,7 +504,10 @@ export class LinearElementEditor { if (clickedPointIndex >= 0) { return null; } - const points = LinearElementEditor.getPointsGlobalCoordinates(element); + const points = LinearElementEditor.getPointsGlobalCoordinates( + element, + elementsMap, + ); if (points.length >= 3 && !appState.editingLinearElement) { return null; } @@ -550,6 +573,7 @@ export class LinearElementEditor { startPoint: Point, endPoint: Point, endPointIndex: number, + elementsMap: ElementsMap, ) { let segmentMidPoint = centerPoint(startPoint, endPoint); if (element.points.length > 2 && element.roundness) { @@ -574,6 +598,7 @@ export class LinearElementEditor { segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates( element, [tx, ty], + elementsMap, ); } } @@ -658,6 +683,7 @@ export class LinearElementEditor { ...element.points, LinearElementEditor.createPointAt( element, + elementsMap, scenePointer.x, scenePointer.y, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, @@ -693,6 +719,7 @@ export class LinearElementEditor { const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( element, + elementsMap, appState.zoom, scenePointer.x, scenePointer.y, @@ -713,11 +740,12 @@ export class LinearElementEditor { element, startBindingElement, endBindingElement, + elementsMap, ); } } - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const targetPoint = @@ -779,6 +807,7 @@ export class LinearElementEditor { scenePointerX: number, scenePointerY: number, appState: AppState, + elementsMap: ElementsMap, ): LinearElementEditor | null { if (!appState.editingLinearElement) { return null; @@ -809,6 +838,7 @@ export class LinearElementEditor { const [width, height] = LinearElementEditor._getShiftLockedDelta( element, + elementsMap, lastCommittedPoint, [scenePointerX, scenePointerY], event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, @@ -821,6 +851,7 @@ export class LinearElementEditor { } else { newPoint = LinearElementEditor.createPointAt( element, + elementsMap, scenePointerX - appState.editingLinearElement.pointerOffset.x, scenePointerY - appState.editingLinearElement.pointerOffset.y, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, @@ -847,8 +878,9 @@ export class LinearElementEditor { static getPointGlobalCoordinates( element: NonDeleted, point: Point, + elementsMap: ElementsMap, ) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; @@ -860,8 +892,9 @@ export class LinearElementEditor { /** scene coords */ static getPointsGlobalCoordinates( element: NonDeleted, + elementsMap: ElementsMap, ): Point[] { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; return element.points.map((point) => { @@ -873,13 +906,15 @@ export class LinearElementEditor { static getPointAtIndexGlobalCoordinates( element: NonDeleted, + indexMaybeFromEnd: number, // -1 for last element + elementsMap: ElementsMap, ): Point { const index = indexMaybeFromEnd < 0 ? element.points.length + indexMaybeFromEnd : indexMaybeFromEnd; - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; @@ -893,8 +928,9 @@ export class LinearElementEditor { static pointFromAbsoluteCoords( element: NonDeleted, absoluteCoords: Point, + elementsMap: ElementsMap, ): Point { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const [x, y] = rotate( @@ -909,12 +945,15 @@ export class LinearElementEditor { static getPointIndexUnderCursor( element: NonDeleted, + elementsMap: ElementsMap, zoom: AppState["zoom"], x: number, y: number, ) { - const pointHandles = - LinearElementEditor.getPointsGlobalCoordinates(element); + const pointHandles = LinearElementEditor.getPointsGlobalCoordinates( + element, + elementsMap, + ); let idx = pointHandles.length; // loop from right to left because points on the right are rendered over // points on the left, thus should take precedence when clicking, if they @@ -934,12 +973,13 @@ export class LinearElementEditor { static createPointAt( element: NonDeleted, + elementsMap: ElementsMap, scenePointerX: number, scenePointerY: number, gridSize: number | null, ): Point { const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize); - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const [rotatedX, rotatedY] = rotate( @@ -1190,6 +1230,7 @@ export class LinearElementEditor { pointerCoords: PointerCoords, appState: AppState, snapToGrid: boolean, + elementsMap: ElementsMap, ) { const element = LinearElementEditor.getElement( linearElementEditor.elementId, @@ -1208,6 +1249,7 @@ export class LinearElementEditor { const midpoint = LinearElementEditor.createPointAt( element, + elementsMap, pointerCoords.x, pointerCoords.y, snapToGrid ? appState.gridSize : null, @@ -1260,6 +1302,7 @@ export class LinearElementEditor { private static _getShiftLockedDelta( element: NonDeleted, + elementsMap: ElementsMap, referencePoint: Point, scenePointer: Point, gridSize: number | null, @@ -1267,6 +1310,7 @@ export class LinearElementEditor { const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( element, referencePoint, + elementsMap, ); const [gridX, gridY] = getGridPoint( @@ -1288,8 +1332,12 @@ export class LinearElementEditor { static getBoundTextElementPosition = ( element: ExcalidrawLinearElement, boundTextElement: ExcalidrawTextElementWithContainer, + elementsMap: ElementsMap, ): { x: number; y: number } => { - const points = LinearElementEditor.getPointsGlobalCoordinates(element); + const points = LinearElementEditor.getPointsGlobalCoordinates( + element, + elementsMap, + ); if (points.length < 2) { mutateElement(boundTextElement, { isDeleted: true }); } @@ -1300,6 +1348,7 @@ export class LinearElementEditor { const midPoint = LinearElementEditor.getPointGlobalCoordinates( element, element.points[index], + elementsMap, ); x = midPoint[0] - boundTextElement.width / 2; y = midPoint[1] - boundTextElement.height / 2; @@ -1319,6 +1368,7 @@ export class LinearElementEditor { points[index], points[index + 1], index + 1, + elementsMap, ); } x = midSegmentMidpoint[0] - boundTextElement.width / 2; @@ -1329,6 +1379,7 @@ export class LinearElementEditor { static getMinMaxXYWithBoundText = ( element: ExcalidrawLinearElement, + elementsMap: ElementsMap, elementBounds: Bounds, boundTextElement: ExcalidrawTextElementWithContainer, ): [number, number, number, number, number, number] => { @@ -1339,6 +1390,7 @@ export class LinearElementEditor { LinearElementEditor.getBoundTextElementPosition( element, boundTextElement, + elementsMap, ); const boundTextX2 = boundTextX1 + boundTextElement.width; const boundTextY2 = boundTextY1 + boundTextElement.height; @@ -1479,6 +1531,7 @@ export class LinearElementEditor { if (boundTextElement) { coords = LinearElementEditor.getMinMaxXYWithBoundText( element, + elementsMap, [x1, y1, x2, y2], boundTextElement, ); diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index f1e0d80939..076f64722d 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -16,6 +16,7 @@ import { ExcalidrawEmbeddableElement, ExcalidrawMagicFrameElement, ExcalidrawIframeElement, + ElementsMap, } from "./types"; import { arrayToMap, @@ -260,6 +261,7 @@ export const newTextElement = ( const getAdjustedDimensions = ( element: ExcalidrawTextElement, + elementsMap: ElementsMap, nextText: string, ): { x: number; @@ -294,7 +296,7 @@ const getAdjustedDimensions = ( x = element.x - offsets.x; y = element.y - offsets.y; } else { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords( element, @@ -335,6 +337,7 @@ const getAdjustedDimensions = ( export const refreshTextDimensions = ( textElement: ExcalidrawTextElement, container: ExcalidrawTextContainer | null, + elementsMap: ElementsMap, text = textElement.text, ) => { if (textElement.isDeleted) { @@ -347,13 +350,14 @@ export const refreshTextDimensions = ( getBoundTextMaxWidth(container, textElement), ); } - const dimensions = getAdjustedDimensions(textElement, text); + const dimensions = getAdjustedDimensions(textElement, elementsMap, text); return { text, ...dimensions }; }; export const updateTextElement = ( textElement: ExcalidrawTextElement, container: ExcalidrawTextContainer | null, + elementsMap: ElementsMap, { text, isDeleted, @@ -367,7 +371,7 @@ export const updateTextElement = ( return newElementWith(textElement, { originalText, isDeleted: isDeleted ?? textElement.isDeleted, - ...refreshTextDimensions(textElement, container, originalText), + ...refreshTextDimensions(textElement, container, elementsMap, originalText), }); }; diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index deb5fead32..49724d9eba 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -86,11 +86,12 @@ export const transformElements = ( if (transformHandleType === "rotation") { rotateSingleElement( element, + elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle, ); - updateBoundElements(element); + updateBoundElements(element, elementsMap); } else if ( isTextElement(element) && (transformHandleType === "nw" || @@ -106,7 +107,7 @@ export const transformElements = ( pointerX, pointerY, ); - updateBoundElements(element); + updateBoundElements(element, elementsMap); } else if (transformHandleType) { resizeSingleElement( originalElements, @@ -157,11 +158,12 @@ export const transformElements = ( const rotateSingleElement = ( element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, pointerX: number, pointerY: number, shouldRotateWithDiscreteAngle: boolean, ) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; let angle: number; @@ -266,7 +268,7 @@ const resizeSingleTextElement = ( pointerX: number, pointerY: number, ) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; // rotation pointer with reverse angle @@ -629,7 +631,7 @@ export const resizeSingleElement = ( ) { mutateElement(element, resizedElement); - updateBoundElements(element, { + updateBoundElements(element, elementsMap, { newSize: { width: resizedElement.width, height: resizedElement.height }, }); @@ -696,7 +698,11 @@ export const resizeMultipleElements = ( if (!isBoundToContainer(text)) { return acc; } - const xy = LinearElementEditor.getBoundTextElementPosition(orig, text); + const xy = LinearElementEditor.getBoundTextElementPosition( + orig, + text, + elementsMap, + ); return [...acc, { ...text, ...xy }]; }, [] as ExcalidrawTextElementWithContainer[]); @@ -879,7 +885,7 @@ export const resizeMultipleElements = ( mutateElement(element, update, false); - updateBoundElements(element, { + updateBoundElements(element, elementsMap, { simultaneouslyUpdated: elementsToUpdate, newSize: { width, height }, }); @@ -921,7 +927,7 @@ const rotateMultipleElements = ( elements .filter((element) => !isFrameLikeElement(element)) .forEach((element) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const origAngle = @@ -942,7 +948,9 @@ const rotateMultipleElements = ( }, false, ); - updateBoundElements(element, { simultaneouslyUpdated: elements }); + updateBoundElements(element, elementsMap, { + simultaneouslyUpdated: elements, + }); const boundText = getBoundTextElement(element, elementsMap); if (boundText && !isArrowElement(element)) { @@ -964,12 +972,13 @@ const rotateMultipleElements = ( export const getResizeOffsetXY = ( transformHandleType: MaybeTransformHandleType, selectedElements: NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, x: number, y: number, ): [number, number] => { const [x1, y1, x2, y2] = selectedElements.length === 1 - ? getElementAbsoluteCoords(selectedElements[0]) + ? getElementAbsoluteCoords(selectedElements[0], elementsMap) : getCommonBounds(selectedElements); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; diff --git a/packages/excalidraw/element/resizeTest.ts b/packages/excalidraw/element/resizeTest.ts index 9947a60825..2e01f94d97 100644 --- a/packages/excalidraw/element/resizeTest.ts +++ b/packages/excalidraw/element/resizeTest.ts @@ -2,6 +2,7 @@ import { ExcalidrawElement, PointerType, NonDeletedExcalidrawElement, + ElementsMap, } from "./types"; import { @@ -27,6 +28,7 @@ const isInsideTransformHandle = ( export const resizeTest = ( element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, appState: AppState, x: number, y: number, @@ -38,7 +40,7 @@ export const resizeTest = ( } const { rotation: rotationTransformHandle, ...transformHandles } = - getTransformHandles(element, zoom, pointerType); + getTransformHandles(element, zoom, elementsMap, pointerType); if ( rotationTransformHandle && @@ -70,6 +72,7 @@ export const getElementWithTransformHandleType = ( scenePointerY: number, zoom: Zoom, pointerType: PointerType, + elementsMap: ElementsMap, ) => { return elements.reduce((result, element) => { if (result) { @@ -77,6 +80,7 @@ export const getElementWithTransformHandleType = ( } const transformHandleType = resizeTest( element, + elementsMap, appState, scenePointerX, scenePointerY, diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index fc4c15f2d1..4aa0868d77 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -53,6 +53,7 @@ const splitIntoLines = (text: string) => { export const redrawTextBoundingBox = ( textElement: ExcalidrawTextElement, container: ExcalidrawElement | null, + elementsMap: ElementsMap, ) => { let maxWidth = undefined; const boundTextUpdates = { @@ -110,7 +111,11 @@ export const redrawTextBoundingBox = ( ...textElement, ...boundTextUpdates, } as ExcalidrawTextElementWithContainer; - const { x, y } = computeBoundTextPosition(container, updatedTextElement); + const { x, y } = computeBoundTextPosition( + container, + updatedTextElement, + elementsMap, + ); boundTextUpdates.x = x; boundTextUpdates.y = y; } @@ -119,11 +124,11 @@ export const redrawTextBoundingBox = ( }; export const bindTextToShapeAfterDuplication = ( - sceneElements: ExcalidrawElement[], + newElements: ExcalidrawElement[], oldElements: ExcalidrawElement[], oldIdToDuplicatedId: Map, ): void => { - const sceneElementMap = arrayToMap(sceneElements) as Map< + const newElementsMap = arrayToMap(newElements) as Map< ExcalidrawElement["id"], ExcalidrawElement >; @@ -134,7 +139,7 @@ export const bindTextToShapeAfterDuplication = ( if (boundTextElementId) { const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId); if (newTextElementId) { - const newContainer = sceneElementMap.get(newElementId); + const newContainer = newElementsMap.get(newElementId); if (newContainer) { mutateElement(newContainer, { boundElements: (element.boundElements || []) @@ -149,7 +154,7 @@ export const bindTextToShapeAfterDuplication = ( }), }); } - const newTextElement = sceneElementMap.get(newTextElementId); + const newTextElement = newElementsMap.get(newTextElementId); if (newTextElement && isTextElement(newTextElement)) { mutateElement(newTextElement, { containerId: newContainer ? newElementId : null, @@ -236,7 +241,7 @@ export const handleBindTextResize = ( if (!isArrowElement(container)) { mutateElement( textElement, - computeBoundTextPosition(container, textElement), + computeBoundTextPosition(container, textElement, elementsMap), ); } } @@ -245,11 +250,13 @@ export const handleBindTextResize = ( export const computeBoundTextPosition = ( container: ExcalidrawElement, boundTextElement: ExcalidrawTextElementWithContainer, + elementsMap: ElementsMap, ) => { if (isArrowElement(container)) { return LinearElementEditor.getBoundTextElementPosition( container, boundTextElement, + elementsMap, ); } const containerCoords = getContainerCoords(container); @@ -698,12 +705,16 @@ export const getContainerCenter = ( y: container.y + container.height / 2, }; } - const points = LinearElementEditor.getPointsGlobalCoordinates(container); + const points = LinearElementEditor.getPointsGlobalCoordinates( + container, + elementsMap, + ); if (points.length % 2 === 1) { const index = Math.floor(container.points.length / 2); const midPoint = LinearElementEditor.getPointGlobalCoordinates( container, container.points[index], + elementsMap, ); return { x: midPoint[0], y: midPoint[1] }; } @@ -719,6 +730,7 @@ export const getContainerCenter = ( points[index], points[index + 1], index + 1, + elementsMap, ); } return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] }; @@ -757,11 +769,13 @@ export const getTextElementAngle = ( export const getBoundTextElementPosition = ( container: ExcalidrawElement, boundTextElement: ExcalidrawTextElementWithContainer, + elementsMap: ElementsMap, ) => { if (isArrowElement(container)) { return LinearElementEditor.getBoundTextElementPosition( container, boundTextElement, + elementsMap, ); } }; @@ -804,6 +818,7 @@ export const getTextBindableContainerAtPosition = ( appState: AppState, x: number, y: number, + elementsMap: ElementsMap, ): ExcalidrawTextContainer | null => { const selectedElements = getSelectedElements(elements, appState); if (selectedElements.length === 1) { @@ -817,7 +832,10 @@ export const getTextBindableContainerAtPosition = ( if (elements[index].isDeleted) { continue; } - const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]); + const [x1, y1, x2, y2] = getElementAbsoluteCoords( + elements[index], + elementsMap, + ); if ( isArrowElement(elements[index]) && isHittingElementNotConsideringBoundingBox( @@ -825,6 +843,7 @@ export const getTextBindableContainerAtPosition = ( appState, null, [x, y], + elementsMap, ) ) { hitElement = elements[index]; diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 1a628dd469..ae30be4e90 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -121,13 +121,13 @@ export const textWysiwyg = ({ return; } const { textAlign, verticalAlign } = updatedTextElement; - + const elementsMap = app.scene.getNonDeletedElementsMap(); if (updatedTextElement && isTextElement(updatedTextElement)) { let coordX = updatedTextElement.x; let coordY = updatedTextElement.y; const container = getContainerElement( updatedTextElement, - app.scene.getElementsMapIncludingDeleted(), + app.scene.getNonDeletedElementsMap(), ); let maxWidth = updatedTextElement.width; @@ -143,6 +143,7 @@ export const textWysiwyg = ({ LinearElementEditor.getBoundTextElementPosition( container, updatedTextElement as ExcalidrawTextElementWithContainer, + elementsMap, ); coordX = boundTextCoords.x; coordY = boundTextCoords.y; @@ -200,6 +201,7 @@ export const textWysiwyg = ({ const { y } = computeBoundTextPosition( container, updatedTextElement as ExcalidrawTextElementWithContainer, + elementsMap, ); coordY = y; } @@ -326,7 +328,7 @@ export const textWysiwyg = ({ } const container = getContainerElement( element, - app.scene.getElementsMapIncludingDeleted(), + app.scene.getNonDeletedElementsMap(), ); const font = getFontString({ @@ -513,7 +515,7 @@ export const textWysiwyg = ({ let text = editable.value; const container = getContainerElement( updateElement, - app.scene.getElementsMapIncludingDeleted(), + app.scene.getNonDeletedElementsMap(), ); if (container) { @@ -541,7 +543,11 @@ export const textWysiwyg = ({ ), }); } - redrawTextBoundingBox(updateElement, container); + redrawTextBoundingBox( + updateElement, + container, + app.scene.getNonDeletedElementsMap(), + ); } onSubmit({ diff --git a/packages/excalidraw/element/transformHandles.ts b/packages/excalidraw/element/transformHandles.ts index 19c60a93f1..aee745530d 100644 --- a/packages/excalidraw/element/transformHandles.ts +++ b/packages/excalidraw/element/transformHandles.ts @@ -1,4 +1,5 @@ import { + ElementsMap, ExcalidrawElement, NonDeletedExcalidrawElement, PointerType, @@ -230,6 +231,8 @@ export const getTransformHandlesFromCoords = ( export const getTransformHandles = ( element: ExcalidrawElement, zoom: Zoom, + elementsMap: ElementsMap, + pointerType: PointerType = "mouse", ): TransformHandles => { // so that when locked element is selected (especially when you toggle lock @@ -267,7 +270,7 @@ export const getTransformHandles = ( ? DEFAULT_TRANSFORM_HANDLE_SPACING + 8 : DEFAULT_TRANSFORM_HANDLE_SPACING; return getTransformHandlesFromCoords( - getElementAbsoluteCoords(element, true), + getElementAbsoluteCoords(element, elementsMap, true), element.angle, zoom, pointerType, diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index c4a5a259da..8f550e86ad 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -65,10 +65,11 @@ export const bindElementsToFramesAfterDuplication = ( export function isElementIntersectingFrame( element: ExcalidrawElement, frame: ExcalidrawFrameLikeElement, + elementsMap: ElementsMap, ) { - const frameLineSegments = getElementLineSegments(frame); + const frameLineSegments = getElementLineSegments(frame, elementsMap); - const elementLineSegments = getElementLineSegments(element); + const elementLineSegments = getElementLineSegments(element, elementsMap); const intersecting = frameLineSegments.some((frameLineSegment) => elementLineSegments.some((elementLineSegment) => @@ -82,9 +83,10 @@ export function isElementIntersectingFrame( export const getElementsCompletelyInFrame = ( elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, + elementsMap: ElementsMap, ) => omitGroupsContainingFrameLikes( - getElementsWithinSelection(elements, frame, false), + getElementsWithinSelection(elements, frame, elementsMap, false), ).filter( (element) => (!isFrameLikeElement(element) && !element.frameId) || @@ -95,8 +97,9 @@ export const isElementContainingFrame = ( elements: readonly ExcalidrawElement[], element: ExcalidrawElement, frame: ExcalidrawFrameLikeElement, + elementsMap: ElementsMap, ) => { - return getElementsWithinSelection(elements, element).some( + return getElementsWithinSelection(elements, element, elementsMap).some( (e) => e.id === frame.id, ); }; @@ -104,13 +107,22 @@ export const isElementContainingFrame = ( export const getElementsIntersectingFrame = ( elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, -) => elements.filter((element) => isElementIntersectingFrame(element, frame)); +) => { + const elementsMap = arrayToMap(elements); + return elements.filter((element) => + isElementIntersectingFrame(element, frame, elementsMap), + ); +}; export const elementsAreInFrameBounds = ( elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, + elementsMap: ElementsMap, ) => { - const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame); + const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords( + frame, + elementsMap, + ); const [elementX1, elementY1, elementX2, elementY2] = getCommonBounds(elements); @@ -126,11 +138,12 @@ export const elementsAreInFrameBounds = ( export const elementOverlapsWithFrame = ( element: ExcalidrawElement, frame: ExcalidrawFrameLikeElement, + elementsMap: ElementsMap, ) => { return ( - elementsAreInFrameBounds([element], frame) || - isElementIntersectingFrame(element, frame) || - isElementContainingFrame([frame], element, frame) + elementsAreInFrameBounds([element], frame, elementsMap) || + isElementIntersectingFrame(element, frame, elementsMap) || + isElementContainingFrame([frame], element, frame, elementsMap) ); }; @@ -140,8 +153,9 @@ export const isCursorInFrame = ( y: number; }, frame: NonDeleted, + elementsMap: ElementsMap, ) => { - const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame); + const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap); return isPointWithinBounds( [fx1, fy1], @@ -155,6 +169,7 @@ export const groupsAreAtLeastIntersectingTheFrame = ( groupIds: readonly string[], frame: ExcalidrawFrameLikeElement, ) => { + const elementsMap = arrayToMap(elements); const elementsInGroup = groupIds.flatMap((groupId) => getElementsInGroup(elements, groupId), ); @@ -165,8 +180,8 @@ export const groupsAreAtLeastIntersectingTheFrame = ( return !!elementsInGroup.find( (element) => - elementsAreInFrameBounds([element], frame) || - isElementIntersectingFrame(element, frame), + elementsAreInFrameBounds([element], frame, elementsMap) || + isElementIntersectingFrame(element, frame, elementsMap), ); }; @@ -175,6 +190,7 @@ export const groupsAreCompletelyOutOfFrame = ( groupIds: readonly string[], frame: ExcalidrawFrameLikeElement, ) => { + const elementsMap = arrayToMap(elements); const elementsInGroup = groupIds.flatMap((groupId) => getElementsInGroup(elements, groupId), ); @@ -186,8 +202,8 @@ export const groupsAreCompletelyOutOfFrame = ( return ( elementsInGroup.find( (element) => - elementsAreInFrameBounds([element], frame) || - isElementIntersectingFrame(element, frame), + elementsAreInFrameBounds([element], frame, elementsMap) || + isElementIntersectingFrame(element, frame, elementsMap), ) === undefined ); }; @@ -258,14 +274,15 @@ export const getElementsInResizingFrame = ( allElements: ExcalidrawElementsIncludingDeleted, frame: ExcalidrawFrameLikeElement, appState: AppState, + elementsMap: ElementsMap, ): ExcalidrawElement[] => { const prevElementsInFrame = getFrameChildren(allElements, frame.id); const nextElementsInFrame = new Set(prevElementsInFrame); const elementsCompletelyInFrame = new Set([ - ...getElementsCompletelyInFrame(allElements, frame), + ...getElementsCompletelyInFrame(allElements, frame, elementsMap), ...prevElementsInFrame.filter((element) => - isElementContainingFrame(allElements, element, frame), + isElementContainingFrame(allElements, element, frame, elementsMap), ), ]); @@ -283,7 +300,7 @@ export const getElementsInResizingFrame = ( ); for (const element of elementsNotCompletelyInFrame) { - if (!isElementIntersectingFrame(element, frame)) { + if (!isElementIntersectingFrame(element, frame, elementsMap)) { if (element.groupIds.length === 0) { nextElementsInFrame.delete(element); } @@ -334,7 +351,7 @@ export const getElementsInResizingFrame = ( if (isSelected) { const elementsInGroup = getElementsInGroup(allElements, id); - if (elementsAreInFrameBounds(elementsInGroup, frame)) { + if (elementsAreInFrameBounds(elementsInGroup, frame, elementsMap)) { for (const element of elementsInGroup) { nextElementsInFrame.add(element); } @@ -348,12 +365,13 @@ export const getElementsInResizingFrame = ( }; export const getElementsInNewFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, + elements: ExcalidrawElementsIncludingDeleted, frame: ExcalidrawFrameLikeElement, + elementsMap: ElementsMap, ) => { return omitGroupsContainingFrameLikes( - allElements, - getElementsCompletelyInFrame(allElements, frame), + elements, + getElementsCompletelyInFrame(elements, frame, elementsMap), ); }; @@ -388,7 +406,7 @@ export const filterElementsEligibleAsFrameChildren = ( frame: ExcalidrawFrameLikeElement, ) => { const otherFrames = new Set(); - + const elementsMap = arrayToMap(elements); elements = omitGroupsContainingFrameLikes(elements); for (const element of elements) { @@ -415,14 +433,18 @@ export const filterElementsEligibleAsFrameChildren = ( if (!processedGroups.has(shallowestGroupId)) { processedGroups.add(shallowestGroupId); const groupElements = getElementsInGroup(elements, shallowestGroupId); - if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) { + if ( + groupElements.some((el) => + elementOverlapsWithFrame(el, frame, elementsMap), + ) + ) { for (const child of groupElements) { eligibleElements.push(child); } } } } else { - const overlaps = elementOverlapsWithFrame(element, frame); + const overlaps = elementOverlapsWithFrame(element, frame, elementsMap); if (overlaps) { eligibleElements.push(element); } @@ -682,12 +704,12 @@ export const getTargetFrame = ( // given an element, return if the element is in some frame export const isElementInFrame = ( element: ExcalidrawElement, - allElements: ElementsMap, + allElementsMap: ElementsMap, appState: StaticCanvasAppState, ) => { - const frame = getTargetFrame(element, allElements, appState); + const frame = getTargetFrame(element, allElementsMap, appState); const _element = isTextElement(element) - ? getContainerElement(element, allElements) || element + ? getContainerElement(element, allElementsMap) || element : element; if (frame) { @@ -703,16 +725,18 @@ export const isElementInFrame = ( } if (_element.groupIds.length === 0) { - return elementOverlapsWithFrame(_element, frame); + return elementOverlapsWithFrame(_element, frame, allElementsMap); } const allElementsInGroup = new Set( - _element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)), + _element.groupIds.flatMap((gid) => + getElementsInGroup(allElementsMap, gid), + ), ); if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) { const selectedElements = new Set( - getSelectedElements(allElements, appState), + getSelectedElements(allElementsMap, appState), ); const editingGroupOverlapsFrame = appState.frameToHighlight !== null; @@ -733,7 +757,7 @@ export const isElementInFrame = ( } for (const elementInGroup of allElementsInGroup) { - if (elementOverlapsWithFrame(elementInGroup, frame)) { + if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) { return true; } } diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index de4bcfe533..a0b8228c99 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -7,6 +7,7 @@ import { ExcalidrawTextElementWithContainer, ExcalidrawFrameLikeElement, NonDeletedSceneElementsMap, + ElementsMap, } from "../element/types"; import { isTextElement, @@ -137,6 +138,7 @@ export interface ExcalidrawElementWithCanvas { const cappedElementCanvasSize = ( element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, zoom: Zoom, ): { width: number; @@ -155,7 +157,7 @@ const cappedElementCanvasSize = ( const padding = getCanvasPadding(element); - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const elementWidth = isLinearElement(element) || isFreeDrawElement(element) ? distance(x1, x2) @@ -200,7 +202,11 @@ const generateElementCanvas = ( const context = canvas.getContext("2d")!; const padding = getCanvasPadding(element); - const { width, height, scale } = cappedElementCanvasSize(element, zoom); + const { width, height, scale } = cappedElementCanvasSize( + element, + elementsMap, + zoom, + ); canvas.width = width; canvas.height = height; @@ -209,7 +215,7 @@ const generateElementCanvas = ( let canvasOffsetY = 0; if (isLinearElement(element) || isFreeDrawElement(element)) { - const [x1, y1] = getElementAbsoluteCoords(element); + const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); canvasOffsetX = element.x > x1 @@ -468,7 +474,7 @@ const drawElementFromCanvas = ( const element = elementWithCanvas.element; const padding = getCanvasPadding(element); const zoom = elementWithCanvas.scale; - let [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + let [x1, y1, x2, y2] = getElementAbsoluteCoords(element, allElementsMap); // Free draw elements will otherwise "shuffle" as the min x and y change if (isFreeDrawElement(element)) { @@ -513,8 +519,10 @@ const drawElementFromCanvas = ( elementWithCanvas.canvas.height, ); - const [, , , , boundTextCx, boundTextCy] = - getElementAbsoluteCoords(boundTextElement); + const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords( + boundTextElement, + allElementsMap, + ); tempCanvasContext.rotate(-element.angle); @@ -694,7 +702,7 @@ export const renderElement = ( ShapeCache.generateElementShape(element, null); if (renderConfig.isExporting) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2 + appState.scrollX; const cy = (y1 + y2) / 2 + appState.scrollY; const shiftX = (x2 - x1) / 2 - (element.x - x1); @@ -737,7 +745,7 @@ export const renderElement = ( // rely on existing shapes ShapeCache.generateElementShape(element, renderConfig); if (renderConfig.isExporting) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2 + appState.scrollX; const cy = (y1 + y2) / 2 + appState.scrollY; let shiftX = (x2 - x1) / 2 - (element.x - x1); @@ -749,6 +757,7 @@ export const renderElement = ( LinearElementEditor.getBoundTextElementPosition( container, element as ExcalidrawTextElementWithContainer, + elementsMap, ); shiftX = (x2 - x1) / 2 - (boundTextCoords.x - x1); shiftY = (y2 - y1) / 2 - (boundTextCoords.y - y1); @@ -804,8 +813,10 @@ export const renderElement = ( tempCanvasContext.rotate(-element.angle); // Shift the canvas to center of bound text - const [, , , , boundTextCx, boundTextCy] = - getElementAbsoluteCoords(boundTextElement); + const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords( + boundTextElement, + elementsMap, + ); const boundTextShiftX = (x1 + x2) / 2 - boundTextCx; const boundTextShiftY = (y1 + y2) / 2 - boundTextCy; tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY); @@ -939,17 +950,18 @@ export const renderElementToSvg = ( renderConfig: SVGRenderConfig, ) => { const offset = { x: offsetX, y: offsetY }; - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); let cx = (x2 - x1) / 2 - (element.x - x1); let cy = (y2 - y1) / 2 - (element.y - y1); if (isTextElement(element)) { const container = getContainerElement(element, elementsMap); if (isArrowElement(container)) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(container); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(container, elementsMap); const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( container, element as ExcalidrawTextElementWithContainer, + elementsMap, ); cx = (x2 - x1) / 2 - (boundTextCoords.x - x1); cy = (y2 - y1) / 2 - (boundTextCoords.y - y1); @@ -1151,6 +1163,7 @@ export const renderElementToSvg = ( const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( element, boundText, + elementsMap, ); const maskX = offsetX + boundTextCoords.x - element.x; diff --git a/packages/excalidraw/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts index d31d696506..d80540fd03 100644 --- a/packages/excalidraw/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -17,6 +17,7 @@ import { GroupId, ExcalidrawBindableElement, ExcalidrawFrameLikeElement, + ElementsMap, } from "../element/types"; import { getElementAbsoluteCoords, @@ -256,7 +257,10 @@ const renderLinearPointHandles = ( context.save(); context.translate(appState.scrollX, appState.scrollY); context.lineWidth = 1 / appState.zoom.value; - const points = LinearElementEditor.getPointsGlobalCoordinates(element); + const points = LinearElementEditor.getPointsGlobalCoordinates( + element, + elementsMap, + ); const { POINT_HANDLE_SIZE } = LinearElementEditor; const radius = appState.editingLinearElement @@ -340,6 +344,7 @@ const highlightPoint = ( const renderLinearElementPointHighlight = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, + elementsMap: ElementsMap, ) => { const { elementId, hoverPointIndex } = appState.selectedLinearElement!; if ( @@ -356,6 +361,7 @@ const renderLinearElementPointHighlight = ( const point = LinearElementEditor.getPointAtIndexGlobalCoordinates( element, hoverPointIndex, + elementsMap, ); context.save(); context.translate(appState.scrollX, appState.scrollY); @@ -510,12 +516,22 @@ const _renderInteractiveScene = ({ appState.suggestedBindings .filter((binding) => binding != null) .forEach((suggestedBinding) => { - renderBindingHighlight(context, appState, suggestedBinding!); + renderBindingHighlight( + context, + appState, + suggestedBinding!, + elementsMap, + ); }); } if (appState.frameToHighlight) { - renderFrameHighlight(context, appState, appState.frameToHighlight); + renderFrameHighlight( + context, + appState, + appState.frameToHighlight, + elementsMap, + ); } if (appState.elementsToHighlight) { @@ -545,7 +561,7 @@ const _renderInteractiveScene = ({ appState.selectedLinearElement && appState.selectedLinearElement.hoverPointIndex >= 0 ) { - renderLinearElementPointHighlight(context, appState); + renderLinearElementPointHighlight(context, appState, elementsMap); } // Paint selected elements if (!appState.multiElement && !appState.editingLinearElement) { @@ -608,7 +624,7 @@ const _renderInteractiveScene = ({ if (selectionColors.length) { const [elementX1, elementY1, elementX2, elementY2, cx, cy] = - getElementAbsoluteCoords(element, true); + getElementAbsoluteCoords(element, elementsMap, true); selections.push({ angle: element.angle, elementX1, @@ -666,7 +682,8 @@ const _renderInteractiveScene = ({ const transformHandles = getTransformHandles( selectedElements[0], appState.zoom, - "mouse", // when we render we don't know which pointer type so use mouse + elementsMap, + "mouse", // when we render we don't know which pointer type so use mouse, ); if (!appState.viewModeEnabled && showBoundingBox) { renderTransformHandles( @@ -953,7 +970,11 @@ const _renderStaticScene = ({ element.groupIds.length > 0 && appState.frameToHighlight && appState.selectedElementIds[element.id] && - (elementOverlapsWithFrame(element, appState.frameToHighlight) || + (elementOverlapsWithFrame( + element, + appState.frameToHighlight, + elementsMap, + ) || element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId))) ) { element.groupIds.forEach((groupId) => @@ -1004,7 +1025,7 @@ const _renderStaticScene = ({ ); } if (!isExporting) { - renderLinkIcon(element, context, appState); + renderLinkIcon(element, context, appState, elementsMap); } } catch (error: any) { console.error(error); @@ -1048,7 +1069,7 @@ const _renderStaticScene = ({ ); } if (!isExporting) { - renderLinkIcon(element, context, appState); + renderLinkIcon(element, context, appState, elementsMap); } }; // - when exporting the whole canvas, we DO NOT apply clipping @@ -1247,6 +1268,7 @@ const renderBindingHighlight = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, suggestedBinding: SuggestedBinding, + elementsMap: ElementsMap, ) => { const renderHighlight = Array.isArray(suggestedBinding) ? renderBindingHighlightForSuggestedPointBinding @@ -1254,7 +1276,7 @@ const renderBindingHighlight = ( context.save(); context.translate(appState.scrollX, appState.scrollY); - renderHighlight(context, suggestedBinding as any); + renderHighlight(context, suggestedBinding as any, elementsMap); context.restore(); }; @@ -1262,8 +1284,9 @@ const renderBindingHighlight = ( const renderBindingHighlightForBindableElement = ( context: CanvasRenderingContext2D, element: ExcalidrawBindableElement, + elementsMap: ElementsMap, ) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const width = x2 - x1; const height = y2 - y1; const threshold = maxBindingGap(element, width, height); @@ -1323,8 +1346,9 @@ const renderFrameHighlight = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, frame: NonDeleted, + elementsMap: ElementsMap, ) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap); const width = x2 - x1; const height = y2 - y1; @@ -1398,6 +1422,7 @@ const renderElementsBoxHighlight = ( const renderBindingHighlightForSuggestedPointBinding = ( context: CanvasRenderingContext2D, suggestedBinding: SuggestedPointBinding, + elementsMap: ElementsMap, ) => { const [element, startOrEnd, bindableElement] = suggestedBinding; @@ -1416,6 +1441,7 @@ const renderBindingHighlightForSuggestedPointBinding = ( const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( element, index, + elementsMap, ); fillCircle(context, x, y, threshold); }); @@ -1426,9 +1452,10 @@ const renderLinkIcon = ( element: NonDeletedExcalidrawElement, context: CanvasRenderingContext2D, appState: StaticCanvasAppState, + elementsMap: ElementsMap, ) => { if (element.link && !appState.selectedElementIds[element.id]) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [x, y, width, height] = getLinkHandleFromCoords( [x1, y1, x2, y2], element.angle, diff --git a/packages/excalidraw/scene/Fonts.ts b/packages/excalidraw/scene/Fonts.ts index 1a97c06e02..6691e90be2 100644 --- a/packages/excalidraw/scene/Fonts.ts +++ b/packages/excalidraw/scene/Fonts.ts @@ -60,10 +60,8 @@ export class Fonts { return newElementWith(element, { ...refreshTextDimensions( element, - getContainerElement( - element, - this.scene.getElementsMapIncludingDeleted(), - ), + getContainerElement(element, this.scene.getNonDeletedElementsMap()), + this.scene.getNonDeletedElementsMap(), ), }); } diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index d463e25971..a8d08c9000 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -392,8 +392,9 @@ export const exportToSvg = async ( const frameElements = getFrameLikeElements(elements); let exportingFrameClipPath = ""; + const elementsMap = arrayToMap(elements); for (const frame of frameElements) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap); const cx = (x2 - x1) / 2 - (frame.x - x1); const cy = (y2 - y1) / 2 - (frame.y - y1); diff --git a/packages/excalidraw/scene/selection.ts b/packages/excalidraw/scene/selection.ts index ae021f6aac..27d4db1c9e 100644 --- a/packages/excalidraw/scene/selection.ts +++ b/packages/excalidraw/scene/selection.ts @@ -1,4 +1,5 @@ import { + ElementsMap, ElementsMapOrArray, ExcalidrawElement, NonDeletedExcalidrawElement, @@ -44,10 +45,11 @@ export const excludeElementsInFramesFromSelection = < export const getElementsWithinSelection = ( elements: readonly NonDeletedExcalidrawElement[], selection: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, excludeElementsInFrames: boolean = true, ) => { const [selectionX1, selectionY1, selectionX2, selectionY2] = - getElementAbsoluteCoords(selection); + getElementAbsoluteCoords(selection, elementsMap); let elementsInSelection = elements.filter((element) => { let [elementX1, elementY1, elementX2, elementY2] = @@ -82,7 +84,7 @@ export const getElementsWithinSelection = ( const containingFrame = getContainingFrame(element); if (containingFrame) { - return elementOverlapsWithFrame(element, containingFrame); + return elementOverlapsWithFrame(element, containingFrame, elementsMap); } return true; diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 7557145ae9..3061c02d45 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -8,15 +8,18 @@ import { import { MaybeTransformHandleType } from "./element/transformHandles"; import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks"; import { + ElementsMap, ExcalidrawElement, NonDeletedExcalidrawElement, } from "./element/types"; import { getMaximumGroups } from "./groups"; import { KEYS } from "./keys"; import { rangeIntersection, rangesOverlap, rotatePoint } from "./math"; -import { getVisibleAndNonSelectedElements } from "./scene/selection"; +import { + getSelectedElements, + getVisibleAndNonSelectedElements, +} from "./scene/selection"; import { AppState, KeyboardModifiersObject, Point } from "./types"; -import { arrayToMap } from "./utils"; const SNAP_DISTANCE = 8; @@ -167,6 +170,7 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => { export const getElementsCorners = ( elements: ExcalidrawElement[], + elementsMap: ElementsMap, { omitCenter, boundingBoxCorners, @@ -185,7 +189,10 @@ export const getElementsCorners = ( if (elements.length === 1) { const element = elements[0]; - let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); + let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + ); if (dragOffset) { x1 += dragOffset.x; @@ -280,6 +287,7 @@ export const getVisibleGaps = ( elements: readonly NonDeletedExcalidrawElement[], selectedElements: ExcalidrawElement[], appState: AppState, + elementsMap: ElementsMap, ) => { const referenceElements: ExcalidrawElement[] = getReferenceElements( elements, @@ -287,10 +295,7 @@ export const getVisibleGaps = ( appState, ); - const referenceBounds = getMaximumGroups( - referenceElements, - arrayToMap(elements), - ) + const referenceBounds = getMaximumGroups(referenceElements, elementsMap) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), @@ -569,19 +574,19 @@ export const getReferenceSnapPoints = ( elements: readonly NonDeletedExcalidrawElement[], selectedElements: ExcalidrawElement[], appState: AppState, + elementsMap: ElementsMap, ) => { const referenceElements = getReferenceElements( elements, selectedElements, appState, ); - - return getMaximumGroups(referenceElements, arrayToMap(elements)) + return getMaximumGroups(referenceElements, elementsMap) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), ) - .flatMap((elementGroup) => getElementsCorners(elementGroup)); + .flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap)); }; const getPointSnaps = ( @@ -641,11 +646,13 @@ const getPointSnaps = ( }; export const snapDraggedElements = ( - selectedElements: ExcalidrawElement[], + elements: ExcalidrawElement[], dragOffset: Vector2D, appState: AppState, event: KeyboardModifiersObject, + elementsMap: ElementsMap, ) => { + const selectedElements = getSelectedElements(elements, appState); if ( !isSnappingEnabled({ appState, event, selectedElements }) || selectedElements.length === 0 @@ -658,7 +665,6 @@ export const snapDraggedElements = ( snapLines: [], }; } - dragOffset.x = round(dragOffset.x); dragOffset.y = round(dragOffset.y); const nearestSnapsX: Snaps = []; @@ -669,7 +675,7 @@ export const snapDraggedElements = ( y: snapDistance, }; - const selectionPoints = getElementsCorners(selectedElements, { + const selectionPoints = getElementsCorners(selectedElements, elementsMap, { dragOffset, }); @@ -719,7 +725,7 @@ export const snapDraggedElements = ( getPointSnaps( selectedElements, - getElementsCorners(selectedElements, { + getElementsCorners(selectedElements, elementsMap, { dragOffset: newDragOffset, }), appState, @@ -1204,6 +1210,7 @@ export const snapNewElement = ( event: KeyboardModifiersObject, origin: Vector2D, dragOffset: Vector2D, + elementsMap: ElementsMap, ) => { if ( !isSnappingEnabled({ event, selectedElements: [draggingElement], appState }) @@ -1248,7 +1255,7 @@ export const snapNewElement = ( nearestSnapsX.length = 0; nearestSnapsY.length = 0; - const corners = getElementsCorners([draggingElement], { + const corners = getElementsCorners([draggingElement], elementsMap, { boundingBoxCorners: true, omitCenter: true, }); @@ -1276,6 +1283,7 @@ export const getSnapLinesAtPointer = ( appState: AppState, pointer: Vector2D, event: KeyboardModifiersObject, + elementsMap: ElementsMap, ) => { if (!isSnappingEnabled({ event, selectedElements: [], appState })) { return { @@ -1301,7 +1309,7 @@ export const getSnapLinesAtPointer = ( const verticalSnapLines: PointerSnapLine[] = []; for (const referenceElement of referenceElements) { - const corners = getElementsCorners([referenceElement]); + const corners = getElementsCorners([referenceElement], elementsMap); for (const corner of corners) { const offsetX = corner[0] - pointer.x; diff --git a/packages/excalidraw/tests/binding.test.tsx b/packages/excalidraw/tests/binding.test.tsx index cb2c4b3400..9e074c2e51 100644 --- a/packages/excalidraw/tests/binding.test.tsx +++ b/packages/excalidraw/tests/binding.test.tsx @@ -5,6 +5,7 @@ import { getTransformHandles } from "../element/transformHandles"; import { API } from "./helpers/api"; import { KEYS } from "../keys"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; +import { arrayToMap } from "../utils"; const { h } = window; @@ -91,8 +92,12 @@ describe("element binding", () => { expect(arrow.startBinding?.elementId).toBe(rectLeft.id); expect(arrow.endBinding?.elementId).toBe(rectRight.id); - const rotation = getTransformHandles(arrow, h.state.zoom, "mouse") - .rotation!; + const rotation = getTransformHandles( + arrow, + h.state.zoom, + arrayToMap(h.elements), + "mouse", + ).rotation!; const rotationHandleX = rotation[0] + rotation[2] / 2; const rotationHandleY = rotation[1] + rotation[3] / 2; mouse.down(rotationHandleX, rotationHandleY); diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx index 875e877525..bd141f6bee 100644 --- a/packages/excalidraw/tests/flip.test.tsx +++ b/packages/excalidraw/tests/flip.test.tsx @@ -27,7 +27,7 @@ import * as blob from "../data/blob"; import { KEYS } from "../keys"; import { getBoundTextElementPosition } from "../element/textElement"; import { createPasteEvent } from "../clipboard"; -import { cloneJSON } from "../utils"; +import { arrayToMap, cloneJSON } from "../utils"; const { h } = window; const mouse = new Pointer("mouse"); @@ -194,9 +194,10 @@ const checkElementsBoundingBox = async ( element2: ExcalidrawElement, toleranceInPx: number = 0, ) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1); + const elementsMap = arrayToMap([element1, element2]); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1, elementsMap); - const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2); + const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2, elementsMap); await waitFor(() => { // Check if width and height did not change @@ -853,7 +854,11 @@ describe("mutliple elements", () => { h.app.actionManager.executeAction(actionFlipVertical); const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer; - const arrowTextPos = getBoundTextElementPosition(arrow.get(), arrowText)!; + const arrowTextPos = getBoundTextElementPosition( + arrow.get(), + arrowText, + arrayToMap(h.elements), + )!; const rectText = h.elements[3] as ExcalidrawTextElementWithContainer; expect(arrow.x).toBeCloseTo(180); diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index 58579fe933..42685b866a 100644 --- a/packages/excalidraw/tests/helpers/ui.ts +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -32,6 +32,7 @@ import { import { getCommonBounds, getElementPointsCoords } from "../../element/bounds"; import { rotatePoint } from "../../math"; import { getTextEditor } from "../queries/dom"; +import { arrayToMap } from "../../utils"; const { h } = window; @@ -286,9 +287,12 @@ const transform = ( let handleCoords: TransformHandle | undefined; if (elements.length === 1) { - handleCoords = getTransformHandles(elements[0], h.state.zoom, "mouse")[ - handle - ]; + handleCoords = getTransformHandles( + elements[0], + h.state.zoom, + arrayToMap(h.elements), + "mouse", + )[handle]; } else { const [x1, y1, x2, y2] = getCommonBounds(elements); const isFrameSelected = elements.some(isFrameLikeElement); diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx index ce0e1c856b..6c01987c98 100644 --- a/packages/excalidraw/tests/linearElementEditor.test.tsx +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -343,6 +343,8 @@ describe("Test Linear Elements", () => { }); it("should update all the midpoints when element position changed", async () => { + const elementsMap = arrayToMap(h.elements); + createThreePointerLinearElement("line", { type: ROUNDNESS.PROPORTIONAL_RADIUS, }); @@ -351,7 +353,10 @@ describe("Test Linear Elements", () => { expect(line.points.length).toEqual(3); enterLineEditingMode(line); - const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); expect([line.x, line.y]).toEqual(points[0]); const midPoints = LinearElementEditor.getEditorMidPoints( @@ -465,7 +470,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 elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); const midPoints = LinearElementEditor.getEditorMidPoints( line, h.app.scene.getNonDeletedElementsMap(), @@ -482,7 +491,10 @@ describe("Test Linear Elements", () => { ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); - const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ points[0][0] - delta, points[0][1] - delta, @@ -499,7 +511,11 @@ describe("Test Linear Elements", () => { }); it("should hide midpoints in the segment when points moved close", async () => { - const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); const midPoints = LinearElementEditor.getEditorMidPoints( line, h.app.scene.getNonDeletedElementsMap(), @@ -516,7 +532,10 @@ describe("Test Linear Elements", () => { ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); - const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ points[0][0] + delta, points[0][1] + delta, @@ -535,7 +554,10 @@ describe("Test Linear Elements", () => { it("should remove the midpoint when one of the points in the segment is deleted", async () => { const line = h.elements[0] as ExcalidrawLinearElement; enterLineEditingMode(line); - const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + arrayToMap(h.elements), + ); // dragging line from last segment midpoint drag(lastSegmentMidpoint, [ @@ -637,7 +659,11 @@ describe("Test Linear Elements", () => { }); it("should update all the midpoints when its point is dragged", async () => { - const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); const midPoints = LinearElementEditor.getEditorMidPoints( line, h.app.scene.getNonDeletedElementsMap(), @@ -649,7 +675,10 @@ describe("Test Linear Elements", () => { // Drag from first point drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]); - const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ points[0][0] - delta, points[0][1] - delta, @@ -678,7 +707,11 @@ describe("Test Linear Elements", () => { }); it("should hide midpoints in the segment when points moved close", async () => { - const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); const midPoints = LinearElementEditor.getEditorMidPoints( line, h.app.scene.getNonDeletedElementsMap(), @@ -695,7 +728,10 @@ describe("Test Linear Elements", () => { ); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); - const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ points[0][0] + delta, points[0][1] + delta, @@ -712,6 +748,8 @@ describe("Test Linear Elements", () => { }); it("should update all the midpoints when a point is deleted", async () => { + const elementsMap = arrayToMap(h.elements); + drag(lastSegmentMidpoint, [ lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta, @@ -723,7 +761,10 @@ describe("Test Linear Elements", () => { h.app.scene.getNonDeletedElementsMap(), h.state, ); - const points = LinearElementEditor.getPointsGlobalCoordinates(line); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); // delete 3rd point deletePoint(points[2]); @@ -837,6 +878,7 @@ describe("Test Linear Elements", () => { const position = LinearElementEditor.getBoundTextElementPosition( container, textElement, + arrayToMap(h.elements), ); expect(position).toMatchInlineSnapshot(` { @@ -859,6 +901,7 @@ describe("Test Linear Elements", () => { const position = LinearElementEditor.getBoundTextElementPosition( container, textElement, + arrayToMap(h.elements), ); expect(position).toMatchInlineSnapshot(` { @@ -893,6 +936,7 @@ describe("Test Linear Elements", () => { const position = LinearElementEditor.getBoundTextElementPosition( container, textElement, + arrayToMap(h.elements), ); expect(position).toMatchInlineSnapshot(` { @@ -1012,8 +1056,13 @@ describe("Test Linear Elements", () => { ); expect(container.width).toBe(70); expect(container.height).toBe(50); - expect(getBoundTextElementPosition(container, textElement)) - .toMatchInlineSnapshot(` + expect( + getBoundTextElementPosition( + container, + textElement, + arrayToMap(h.elements), + ), + ).toMatchInlineSnapshot(` { "x": 75, "y": 60, @@ -1051,8 +1100,13 @@ describe("Test Linear Elements", () => { } `); - expect(getBoundTextElementPosition(container, textElement)) - .toMatchInlineSnapshot(` + expect( + getBoundTextElementPosition( + container, + textElement, + arrayToMap(h.elements), + ), + ).toMatchInlineSnapshot(` { "x": 271.11716195150507, "y": 45, @@ -1090,7 +1144,8 @@ describe("Test Linear Elements", () => { arrow, ); expect(container.width).toBe(40); - expect(getBoundTextElementPosition(container, textElement)) + const elementsMap = arrayToMap(h.elements); + expect(getBoundTextElementPosition(container, textElement, elementsMap)) .toMatchInlineSnapshot(` { "x": 25, @@ -1102,7 +1157,10 @@ describe("Test Linear Elements", () => { collaboration made easy" `); - const points = LinearElementEditor.getPointsGlobalCoordinates(container); + const points = LinearElementEditor.getPointsGlobalCoordinates( + container, + elementsMap, + ); // Drag from last point drag(points[1], [points[1][0] + 300, points[1][1]]); @@ -1115,7 +1173,7 @@ describe("Test Linear Elements", () => { } `); - expect(getBoundTextElementPosition(container, textElement)) + expect(getBoundTextElementPosition(container, textElement, elementsMap)) .toMatchInlineSnapshot(` { "x": 75, diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index 22d828ee95..625175700a 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -13,6 +13,7 @@ import { import { UI, Pointer, Keyboard } from "./helpers/ui"; import { KEYS } from "../keys"; import { vi } from "vitest"; +import { arrayToMap } from "../utils"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -75,12 +76,13 @@ describe("move element", () => { const rectA = UI.createElement("rectangle", { size: 100 }); const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const line = UI.createElement("line", { x: 110, y: 50, size: 80 }); - + const elementsMap = arrayToMap(h.elements); // bind line to two rectangles bindOrUnbindLinearElement( line.get() as NonDeleted, rectA.get() as ExcalidrawRectangleElement, rectB.get() as ExcalidrawRectangleElement, + elementsMap, ); // select the second rectangles diff --git a/packages/excalidraw/tests/resize.test.tsx b/packages/excalidraw/tests/resize.test.tsx index 43e84d0cee..d4d1e7673f 100644 --- a/packages/excalidraw/tests/resize.test.tsx +++ b/packages/excalidraw/tests/resize.test.tsx @@ -13,6 +13,7 @@ import { API } from "./helpers/api"; import { KEYS } from "../keys"; import { isLinearElement } from "../element/typeChecks"; import { LinearElementEditor } from "../element/linearElementEditor"; +import { arrayToMap } from "../utils"; ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -301,10 +302,12 @@ describe("arrow element", () => { ], }); const label = await UI.editText(arrow, "Hello"); + const elementsMap = arrayToMap(h.elements); UI.resize(arrow, "se", [50, 30]); let labelPos = LinearElementEditor.getBoundTextElementPosition( arrow, label, + elementsMap, ); expect(labelPos.x + label.width / 2).toBeCloseTo( @@ -317,7 +320,11 @@ describe("arrow element", () => { expect(label.fontSize).toEqual(20); UI.resize(arrow, "w", [20, 0]); - labelPos = LinearElementEditor.getBoundTextElementPosition(arrow, label); + labelPos = LinearElementEditor.getBoundTextElementPosition( + arrow, + label, + elementsMap, + ); expect(labelPos.x + label.width / 2).toBeCloseTo( arrow.x + arrow.points[2][0], @@ -743,15 +750,17 @@ describe("multiple selection", () => { const selectionTop = 20 - topArrowLabel.height / 2; const move = [80, 0] as [number, number]; const scale = move[0] / selectionWidth + 1; - + const elementsMap = arrayToMap(h.elements); UI.resize([topArrow.get(), bottomArrow.get()], "se", move); const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( topArrow, topArrowLabel, + elementsMap, ); const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( bottomArrow, bottomArrowLabel, + elementsMap, ); expect(topArrow.x).toBeCloseTo(0); @@ -944,12 +953,13 @@ describe("multiple selection", () => { const scaleX = move[0] / selectionWidth + 1; const scaleY = -scaleX; const lineOrigBounds = getBoundsFromPoints(line); - + const elementsMap = arrayToMap(h.elements); UI.resize([line, image, rectangle, boundArrow], "se", move); const lineNewBounds = getBoundsFromPoints(line); const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition( boundArrow, arrowLabel, + elementsMap, ); expect(line.x).toBeCloseTo(60 * scaleX);