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
This commit is contained in:
Aakansha Doshi 2024-02-16 11:35:01 +05:30 committed by GitHub
parent 73bf50e8a8
commit 47f87f4ecb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 779 additions and 270 deletions

View file

@ -58,7 +58,11 @@ export const actionUnbindText = register({
element.id, element.id,
); );
resetOriginalContainerCache(element.id); resetOriginalContainerCache(element.id);
const { x, y } = computeBoundTextPosition(element, boundTextElement); const { x, y } = computeBoundTextPosition(
element,
boundTextElement,
elementsMap,
);
mutateElement(boundTextElement as ExcalidrawTextElement, { mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null, containerId: null,
width, width,
@ -145,7 +149,11 @@ export const actionBindText = register({
}), }),
}); });
const originalContainerHeight = container.height; const originalContainerHeight = container.height;
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
);
// overwritting the cache with original container height so // overwritting the cache with original container height so
// it can be restored when unbind // it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight); updateOriginalContainerCache(container.id, originalContainerHeight);
@ -286,7 +294,11 @@ export const actionWrapTextInContainer = register({
}, },
false, false,
); );
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
);
updatedElements = pushContainerBelowText( updatedElements = pushContainerBelowText(
[...updatedElements, container], [...updatedElements, container],

View file

@ -1,6 +1,6 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isInvisiblySmallElement } from "../element"; import { isInvisiblySmallElement } from "../element";
import { updateActiveTool } from "../utils"; import { arrayToMap, updateActiveTool } from "../utils";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons"; import { done } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
@ -26,6 +26,8 @@ export const actionFinalize = register({
_, _,
{ interactiveCanvas, focusContainer, scene }, { interactiveCanvas, focusContainer, scene },
) => { ) => {
const elementsMap = arrayToMap(elements);
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } = const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement; appState.editingLinearElement;
@ -37,6 +39,7 @@ export const actionFinalize = register({
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
); );
} }
return { return {
@ -125,12 +128,14 @@ export const actionFinalize = register({
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiPointElement, multiPointElement,
-1, -1,
arrayToMap(elements),
); );
maybeBindLinearElement( maybeBindLinearElement(
multiPointElement, multiPointElement,
appState, appState,
Scene.getScene(multiPointElement)!, Scene.getScene(multiPointElement)!,
{ x, y }, { x, y },
elementsMap,
); );
} }
} }

View file

@ -115,7 +115,7 @@ const flipElements = (
(isBindingEnabled(appState) (isBindingEnabled(appState)
? bindOrUnbindSelectedElements ? bindOrUnbindSelectedElements
: unbindLinearElements)(selectedElements); : unbindLinearElements)(selectedElements, elementsMap);
return selectedElements; return selectedElements;
}; };

View file

@ -180,6 +180,8 @@ export const actionUngroup = register({
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const groupIds = getSelectedGroupIds(appState); const groupIds = getSelectedGroupIds(appState);
const elementsMap = arrayToMap(elements);
if (groupIds.length === 0) { if (groupIds.length === 0) {
return { appState, elements, commitToHistory: false }; return { appState, elements, commitToHistory: false };
} }
@ -226,7 +228,12 @@ export const actionUngroup = register({
if (frame) { if (frame) {
nextElements = replaceAllElementsInFrame( nextElements = replaceAllElementsInFrame(
nextElements, nextElements,
getElementsInResizingFrame(nextElements, frame, appState), getElementsInResizingFrame(
nextElements,
frame,
appState,
elementsMap,
),
frame, frame,
app, app,
); );

View file

@ -209,6 +209,7 @@ const changeFontSize = (
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
newElement = offsetElementAfterFontResize(oldElement, newElement); newElement = offsetElementAfterFontResize(oldElement, newElement);
@ -730,6 +731,7 @@ export const actionChangeFontFamily = register({
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
return newElement; return newElement;
} }
@ -829,6 +831,7 @@ export const actionChangeTextAlign = register({
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
return newElement; return newElement;
} }
@ -918,6 +921,7 @@ export const actionChangeVerticalAlign = register({
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
return newElement; return newElement;
} }

View file

@ -128,7 +128,11 @@ export const actionPasteStyles = register({
element.id === newElement.containerId, element.id === newElement.containerId,
) || null; ) || null;
} }
redrawTextBoundingBox(newElement, container); redrawTextBoundingBox(
newElement,
container,
app.scene.getNonDeletedElementsMap(),
);
} }
if ( if (

View file

@ -1536,6 +1536,7 @@ class App extends React.Component<AppProps, AppState> {
<Hyperlink <Hyperlink
key={firstSelectedElement.id} key={firstSelectedElement.id}
element={firstSelectedElement} element={firstSelectedElement}
elementsMap={allElementsMap}
setAppState={this.setAppState} setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen} onLinkOpen={this.props.onLinkOpen}
setToast={this.setToast} setToast={this.setToast}
@ -1549,6 +1550,7 @@ class App extends React.Component<AppProps, AppState> {
isMagicFrameElement(firstSelectedElement) && ( isMagicFrameElement(firstSelectedElement) && (
<ElementCanvasButtons <ElementCanvasButtons
element={firstSelectedElement} element={firstSelectedElement}
elementsMap={elementsMap}
> >
<ElementCanvasButton <ElementCanvasButton
title={t("labels.convertToCode")} title={t("labels.convertToCode")}
@ -1569,6 +1571,7 @@ class App extends React.Component<AppProps, AppState> {
?.status === "done" && ( ?.status === "done" && (
<ElementCanvasButtons <ElementCanvasButtons
element={firstSelectedElement} element={firstSelectedElement}
elementsMap={elementsMap}
> >
<ElementCanvasButton <ElementCanvasButton
title={t("labels.copySource")} title={t("labels.copySource")}
@ -2599,10 +2602,10 @@ class App extends React.Component<AppProps, AppState> {
componentDidUpdate(prevProps: AppProps, prevState: AppState) { componentDidUpdate(prevProps: AppProps, prevState: AppState) {
this.updateEmbeddables(); this.updateEmbeddables();
if ( const elements = this.scene.getElementsIncludingDeleted();
!this.state.showWelcomeScreen && const elementsMap = this.scene.getElementsMapIncludingDeleted();
!this.scene.getElementsIncludingDeleted().length
) { if (!this.state.showWelcomeScreen && !elements.length) {
this.setState({ showWelcomeScreen: true }); this.setState({ showWelcomeScreen: true });
} }
@ -2756,27 +2759,21 @@ class App extends React.Component<AppProps, AppState> {
LinearElementEditor.getPointAtIndexGlobalCoordinates( LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiElement, multiElement,
-1, -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 // 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 // potential issues, this fixes a case where the tab isn't focused during
// init, which would trigger onChange with empty elements, which would then // init, which would trigger onChange with empty elements, which would then
// override whatever is in localStorage currently. // override whatever is in localStorage currently.
if (!this.state.isLoading) { if (!this.state.isLoading) {
this.props.onChange?.( this.props.onChange?.(elements, this.state, this.files);
this.scene.getElementsIncludingDeleted(), this.onChangeEmitter.trigger(elements, this.state, this.files);
this.state,
this.files,
);
this.onChangeEmitter.trigger(
this.scene.getElementsIncludingDeleted(),
this.state,
this.files,
);
} }
} }
@ -3126,7 +3123,11 @@ class App extends React.Component<AppProps, AppState> {
newElement, newElement,
this.scene.getElementsMapIncludingDeleted(), this.scene.getElementsMapIncludingDeleted(),
); );
redrawTextBoundingBox(newElement, container); redrawTextBoundingBox(
newElement,
container,
this.scene.getElementsMapIncludingDeleted(),
);
} }
}); });
@ -3836,7 +3837,7 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + offsetY, y: element.y + offsetY,
}); });
updateBoundElements(element, { updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
}); });
}); });
@ -4010,9 +4011,10 @@ class App extends React.Component<AppProps, AppState> {
} }
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
const elementsMap = this.scene.getNonDeletedElementsMap();
isBindingEnabled(this.state) isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements) ? bindOrUnbindSelectedElements(selectedElements, elementsMap)
: unbindLinearElements(selectedElements); : unbindLinearElements(selectedElements, elementsMap);
this.setState({ suggestedBindings: [] }); this.setState({ suggestedBindings: [] });
} }
}); });
@ -4193,20 +4195,21 @@ class App extends React.Component<AppProps, AppState> {
isExistingElement?: boolean; isExistingElement?: boolean;
}, },
) { ) {
const elementsMap = this.scene.getElementsMapIncludingDeleted();
const updateElement = ( const updateElement = (
text: string, text: string,
originalText: string, originalText: string,
isDeleted: boolean, isDeleted: boolean,
) => { ) => {
this.scene.replaceAllElements([ this.scene.replaceAllElements([
// Not sure why we include deleted elements as well hence using deleted elements map
...this.scene.getElementsIncludingDeleted().map((_element) => { ...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) { if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement( return updateTextElement(
_element, _element,
getContainerElement( getContainerElement(_element, elementsMap),
_element, elementsMap,
this.scene.getElementsMapIncludingDeleted(),
),
{ {
text, text,
isDeleted, isDeleted,
@ -4238,7 +4241,7 @@ class App extends React.Component<AppProps, AppState> {
onChange: withBatchedUpdates((text) => { onChange: withBatchedUpdates((text) => {
updateElement(text, text, false); updateElement(text, text, false);
if (isNonDeletedElement(element)) { if (isNonDeletedElement(element)) {
updateBoundElements(element); updateBoundElements(element, elementsMap);
} }
}), }),
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => { onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
@ -4377,6 +4380,7 @@ class App extends React.Component<AppProps, AppState> {
!(isTextElement(element) && element.containerId)), !(isTextElement(element) && element.containerId)),
); );
const elementsMap = this.scene.getNonDeletedElementsMap();
return getElementsAtPosition(elements, (element) => return getElementsAtPosition(elements, (element) =>
hitTest( hitTest(
element, element,
@ -4384,7 +4388,7 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache, this.frameNameBoundsCache,
x, x,
y, y,
this.scene.getNonDeletedElementsMap(), elementsMap,
), ),
).filter((element) => { ).filter((element) => {
// hitting a frame's element from outside the frame is not considered a hit // hitting a frame's element from outside the frame is not considered a hit
@ -4392,7 +4396,7 @@ class App extends React.Component<AppProps, AppState> {
return containingFrame && return containingFrame &&
this.state.frameRendering.enabled && this.state.frameRendering.enabled &&
this.state.frameRendering.clip this.state.frameRendering.clip
? isCursorInFrame({ x, y }, containingFrame) ? isCursorInFrame({ x, y }, containingFrame, elementsMap)
: true; : true;
}); });
} }
@ -4637,6 +4641,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
sceneX, sceneX,
sceneY, sceneY,
this.scene.getNonDeletedElementsMap(),
); );
if (container) { if (container) {
@ -4648,6 +4653,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
this.frameNameBoundsCache, this.frameNameBoundsCache,
[sceneX, sceneY], [sceneX, sceneY],
this.scene.getNonDeletedElementsMap(),
) )
) { ) {
const midPoint = getContainerCenter( const midPoint = getContainerCenter(
@ -4688,6 +4694,7 @@ class App extends React.Component<AppProps, AppState> {
index <= hitElementIndex && index <= hitElementIndex &&
isPointHittingLink( isPointHittingLink(
element, element,
this.scene.getNonDeletedElementsMap(),
this.state, this.state,
[scenePointer.x, scenePointer.y], [scenePointer.x, scenePointer.y],
this.device.editor.isMobile, this.device.editor.isMobile,
@ -4718,8 +4725,10 @@ class App extends React.Component<AppProps, AppState> {
this.lastPointerDownEvent!, this.lastPointerDownEvent!,
this.state, this.state,
); );
const elementsMap = this.scene.getNonDeletedElementsMap();
const lastPointerDownHittingLinkIcon = isPointHittingLink( const lastPointerDownHittingLinkIcon = isPointHittingLink(
this.hitLinkElement, this.hitLinkElement,
elementsMap,
this.state, this.state,
[lastPointerDownCoords.x, lastPointerDownCoords.y], [lastPointerDownCoords.x, lastPointerDownCoords.y],
this.device.editor.isMobile, this.device.editor.isMobile,
@ -4730,6 +4739,7 @@ class App extends React.Component<AppProps, AppState> {
); );
const lastPointerUpHittingLinkIcon = isPointHittingLink( const lastPointerUpHittingLinkIcon = isPointHittingLink(
this.hitLinkElement, this.hitLinkElement,
elementsMap,
this.state, this.state,
[lastPointerUpCoords.x, lastPointerUpCoords.y], [lastPointerUpCoords.x, lastPointerUpCoords.y],
this.device.editor.isMobile, this.device.editor.isMobile,
@ -4766,10 +4776,11 @@ class App extends React.Component<AppProps, AppState> {
x: number; x: number;
y: number; y: number;
}) => { }) => {
const elementsMap = this.scene.getNonDeletedElementsMap();
const frames = this.scene const frames = this.scene
.getNonDeletedFramesLikes() .getNonDeletedFramesLikes()
.filter((frame): frame is ExcalidrawFrameLikeElement => .filter((frame): frame is ExcalidrawFrameLikeElement =>
isCursorInFrame(sceneCoords, frame), isCursorInFrame(sceneCoords, frame, elementsMap),
); );
return frames.length ? frames[frames.length - 1] : null; return frames.length ? frames[frames.length - 1] : null;
@ -4873,6 +4884,7 @@ class App extends React.Component<AppProps, AppState> {
y: scenePointerY, y: scenePointerY,
}, },
event, event,
this.scene.getNonDeletedElementsMap(),
); );
this.setState((prevState) => { this.setState((prevState) => {
@ -4912,6 +4924,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
); );
if ( if (
@ -5062,6 +5075,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY, scenePointerY,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.scene.getNonDeletedElementsMap(),
); );
if ( if (
elementWithTransformHandleType && elementWithTransformHandleType &&
@ -5109,7 +5123,11 @@ class App extends React.Component<AppProps, AppState> {
!this.state.selectedElementIds[this.hitLinkElement.id] !this.state.selectedElementIds[this.hitLinkElement.id]
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
showHyperlinkTooltip(this.hitLinkElement, this.state); showHyperlinkTooltip(
this.hitLinkElement,
this.state,
this.scene.getNonDeletedElementsMap(),
);
} else { } else {
hideHyperlinkToolip(); hideHyperlinkToolip();
if ( if (
@ -5305,10 +5323,12 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
this.frameNameBoundsCache, this.frameNameBoundsCache,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
elementsMap,
) )
) { ) {
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor( hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element, element,
elementsMap,
this.state.zoom, this.state.zoom,
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
@ -5738,10 +5758,12 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
clicklength < 300 && clicklength < 300 &&
isIframeLikeElement(this.hitLinkElement) && isIframeLikeElement(this.hitLinkElement) &&
!isPointHittingLinkIcon(this.hitLinkElement, this.state, [ !isPointHittingLinkIcon(
scenePointer.x, this.hitLinkElement,
scenePointer.y, this.scene.getNonDeletedElementsMap(),
]) this.state,
[scenePointer.x, scenePointer.y],
)
) { ) {
this.handleEmbeddableCenterClick(this.hitLinkElement); this.handleEmbeddableCenterClick(this.hitLinkElement);
} else { } else {
@ -6039,7 +6061,9 @@ class App extends React.Component<AppProps, AppState> {
): boolean => { ): boolean => {
if (this.state.activeTool.type === "selection") { if (this.state.activeTool.type === "selection") {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
const elementsMap = this.scene.getNonDeletedElementsMap();
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
const elementWithTransformHandleType = const elementWithTransformHandleType =
getElementWithTransformHandleType( getElementWithTransformHandleType(
@ -6049,6 +6073,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.scene.getNonDeletedElementsMap(),
); );
if (elementWithTransformHandleType != null) { if (elementWithTransformHandleType != null) {
this.setState({ this.setState({
@ -6072,6 +6097,7 @@ class App extends React.Component<AppProps, AppState> {
getResizeOffsetXY( getResizeOffsetXY(
pointerDownState.resize.handleType, pointerDownState.resize.handleType,
selectedElements, selectedElements,
elementsMap,
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
), ),
@ -6352,6 +6378,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
sceneX, sceneX,
sceneY, sceneY,
this.scene.getNonDeletedElementsMap(),
); );
if (hasBoundTextElement(element)) { if (hasBoundTextElement(element)) {
@ -6846,6 +6873,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
selectedElements, selectedElements,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
), ),
); );
} }
@ -6869,6 +6897,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
selectedElements, selectedElements,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
), ),
); );
} }
@ -6985,6 +7014,7 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords, pointerCoords,
this.state, this.state,
!event[KEYS.CTRL_OR_CMD], !event[KEYS.CTRL_OR_CMD],
this.scene.getNonDeletedElementsMap(),
); );
if (!ret) { if (!ret) {
return; return;
@ -7143,10 +7173,11 @@ class App extends React.Component<AppProps, AppState> {
this.maybeCacheReferenceSnapPoints(event, selectedElements); this.maybeCacheReferenceSnapPoints(event, selectedElements);
const { snapOffset, snapLines } = snapDraggedElements( const { snapOffset, snapLines } = snapDraggedElements(
getSelectedElements(originalElements, this.state), originalElements,
dragOffset, dragOffset,
this.state, this.state,
event, event,
this.scene.getNonDeletedElementsMap(),
); );
this.setState({ snapLines }); this.setState({ snapLines });
@ -7330,6 +7361,7 @@ class App extends React.Component<AppProps, AppState> {
event, event,
this.state, this.state,
this.setState.bind(this), this.setState.bind(this),
this.scene.getNonDeletedElementsMap(),
); );
// regular box-select // regular box-select
} else { } else {
@ -7360,6 +7392,7 @@ class App extends React.Component<AppProps, AppState> {
const elementsWithinSelection = getElementsWithinSelection( const elementsWithinSelection = getElementsWithinSelection(
elements, elements,
draggingElement, draggingElement,
this.scene.getNonDeletedElementsMap(),
); );
this.setState((prevState) => { this.setState((prevState) => {
@ -7491,7 +7524,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
selectedElementsAreBeingDragged: false, selectedElementsAreBeingDragged: false,
}); });
const elementsMap = this.scene.getNonDeletedElementsMap();
// Handle end of dragging a point of a linear element, might close a loop // Handle end of dragging a point of a linear element, might close a loop
// and sets binding element // and sets binding element
if (this.state.editingLinearElement) { if (this.state.editingLinearElement) {
@ -7506,6 +7539,7 @@ class App extends React.Component<AppProps, AppState> {
childEvent, childEvent,
this.state.editingLinearElement, this.state.editingLinearElement,
this.state, this.state,
elementsMap,
); );
if (editingLinearElement !== this.state.editingLinearElement) { if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({ this.setState({
@ -7529,6 +7563,7 @@ class App extends React.Component<AppProps, AppState> {
childEvent, childEvent,
this.state.selectedLinearElement, this.state.selectedLinearElement,
this.state, this.state,
elementsMap,
); );
const { startBindingElement, endBindingElement } = const { startBindingElement, endBindingElement } =
@ -7539,6 +7574,7 @@ class App extends React.Component<AppProps, AppState> {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
); );
} }
@ -7678,6 +7714,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
this.scene, this.scene,
pointerCoords, pointerCoords,
elementsMap,
); );
} }
this.setState({ suggestedBindings: [], startBoundElement: null }); this.setState({ suggestedBindings: [], startBoundElement: null });
@ -7748,7 +7785,13 @@ class App extends React.Component<AppProps, AppState> {
const frame = getContainingFrame(linearElement); const frame = getContainingFrame(linearElement);
if (frame && linearElement) { if (frame && linearElement) {
if (!elementOverlapsWithFrame(linearElement, frame)) { if (
!elementOverlapsWithFrame(
linearElement,
frame,
this.scene.getNonDeletedElementsMap(),
)
) {
// remove the linear element from all groups // remove the linear element from all groups
// before removing it from the frame as well // before removing it from the frame as well
mutateElement(linearElement, { mutateElement(linearElement, {
@ -7859,6 +7902,7 @@ class App extends React.Component<AppProps, AppState> {
const elementsInsideFrame = getElementsInNewFrame( const elementsInsideFrame = getElementsInNewFrame(
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
draggingElement, draggingElement,
this.scene.getNonDeletedElementsMap(),
); );
this.scene.replaceAllElements( this.scene.replaceAllElements(
@ -7909,6 +7953,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
frame, frame,
this.state, this.state,
elementsMap,
), ),
frame, frame,
this, this,
@ -8189,7 +8234,10 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
(isBindingEnabled(this.state) (isBindingEnabled(this.state)
? bindOrUnbindSelectedElements ? bindOrUnbindSelectedElements
: unbindLinearElements)(this.scene.getSelectedElements(this.state)); : unbindLinearElements)(
this.scene.getSelectedElements(this.state),
elementsMap,
);
} }
if (activeTool.type === "laser") { if (activeTool.type === "laser") {
@ -8719,7 +8767,10 @@ class App extends React.Component<AppProps, AppState> {
if (selectedElements.length > 50) { if (selectedElements.length > 50) {
return; return;
} }
const suggestedBindings = getEligibleElementsForBinding(selectedElements); const suggestedBindings = getEligibleElementsForBinding(
selectedElements,
this.scene.getNonDeletedElementsMap(),
);
this.setState({ suggestedBindings }); this.setState({ suggestedBindings });
} }
@ -9058,6 +9109,7 @@ class App extends React.Component<AppProps, AppState> {
x: gridX - pointerDownState.originInGrid.x, x: gridX - pointerDownState.originInGrid.x,
y: gridY - pointerDownState.originInGrid.y, y: gridY - pointerDownState.originInGrid.y,
}, },
this.scene.getNonDeletedElementsMap(),
); );
gridX += snapOffset.x; gridX += snapOffset.x;
@ -9096,6 +9148,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
draggingElement as ExcalidrawFrameLikeElement, draggingElement as ExcalidrawFrameLikeElement,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
), ),
}); });
} }
@ -9215,6 +9268,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
frame, frame,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
).forEach((element) => elementsToHighlight.add(element)); ).forEach((element) => elementsToHighlight.add(element));
}); });

View file

@ -462,6 +462,7 @@ export const restoreElements = (
refreshTextDimensions( refreshTextDimensions(
element, element,
getContainerElement(element, restoredElementsMap), getContainerElement(element, restoredElementsMap),
restoredElementsMap,
), ),
); );
} }

View file

@ -222,7 +222,7 @@ const bindTextToContainer = (
}), }),
}); });
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(textElement, container, elementsMap);
return [container, textElement] as const; return [container, textElement] as const;
}; };
@ -231,6 +231,7 @@ const bindLinearElementToElement = (
start: ValidLinearElement["start"], start: ValidLinearElement["start"],
end: ValidLinearElement["end"], end: ValidLinearElement["end"],
elementStore: ElementStore, elementStore: ElementStore,
elementsMap: ElementsMap,
): { ): {
linearElement: ExcalidrawLinearElement; linearElement: ExcalidrawLinearElement;
startBoundElement?: ExcalidrawElement; startBoundElement?: ExcalidrawElement;
@ -316,6 +317,7 @@ const bindLinearElementToElement = (
linearElement, linearElement,
startBoundElement as ExcalidrawBindableElement, startBoundElement as ExcalidrawBindableElement,
"start", "start",
elementsMap,
); );
} }
} }
@ -390,6 +392,7 @@ const bindLinearElementToElement = (
linearElement, linearElement,
endBoundElement as ExcalidrawBindableElement, endBoundElement as ExcalidrawBindableElement,
"end", "end",
elementsMap,
); );
} }
} }
@ -612,6 +615,7 @@ export const convertToExcalidrawElements = (
} }
} }
const elementsMap = arrayToMap(elementStore.getElements());
// Add labels and arrow bindings // Add labels and arrow bindings
for (const [id, element] of elementsWithIds) { for (const [id, element] of elementsWithIds) {
const excalidrawElement = elementStore.getElement(id)!; const excalidrawElement = elementStore.getElement(id)!;
@ -625,7 +629,7 @@ export const convertToExcalidrawElements = (
let [container, text] = bindTextToContainer( let [container, text] = bindTextToContainer(
excalidrawElement, excalidrawElement,
element?.label, element?.label,
arrayToMap(elementStore.getElements()), elementsMap,
); );
elementStore.add(container); elementStore.add(container);
elementStore.add(text); elementStore.add(text);
@ -653,6 +657,7 @@ export const convertToExcalidrawElements = (
originalStart, originalStart,
originalEnd, originalEnd,
elementStore, elementStore,
elementsMap,
); );
container = linearElement; container = linearElement;
elementStore.add(linearElement); elementStore.add(linearElement);
@ -677,6 +682,7 @@ export const convertToExcalidrawElements = (
start, start,
end, end,
elementStore, elementStore,
elementsMap,
); );
elementStore.add(linearElement); elementStore.add(linearElement);

View file

@ -1,6 +1,6 @@
import { AppState } from "../types"; import { AppState } from "../types";
import { sceneCoordsToViewportCoords } from "../utils"; import { sceneCoordsToViewportCoords } from "../utils";
import { NonDeletedExcalidrawElement } from "./types"; import { ElementsMap, NonDeletedExcalidrawElement } from "./types";
import { getElementAbsoluteCoords } from "."; import { getElementAbsoluteCoords } from ".";
import { useExcalidrawAppState } from "../components/App"; import { useExcalidrawAppState } from "../components/App";
@ -11,8 +11,9 @@ const CONTAINER_PADDING = 5;
const getContainerCoords = ( const getContainerCoords = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1] = getElementAbsoluteCoords(element); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width, sceneY: y1 }, { sceneX: x1 + element.width, sceneY: y1 },
appState, appState,
@ -25,9 +26,11 @@ const getContainerCoords = (
export const ElementCanvasButtons = ({ export const ElementCanvasButtons = ({
children, children,
element, element,
elementsMap,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
elementsMap: ElementsMap;
}) => { }) => {
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
@ -42,7 +45,7 @@ export const ElementCanvasButtons = ({
return null; return null;
} }
const { x, y } = getContainerCoords(element, appState); const { x, y } = getContainerCoords(element, appState, elementsMap);
return ( return (
<div <div

View file

@ -8,6 +8,7 @@ import {
import { getEmbedLink, embeddableURLValidator } from "./embeddable"; import { getEmbedLink, embeddableURLValidator } from "./embeddable";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { import {
ElementsMap,
ExcalidrawEmbeddableElement, ExcalidrawEmbeddableElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./types"; } from "./types";
@ -60,12 +61,14 @@ const embeddableLinkCache = new Map<
export const Hyperlink = ({ export const Hyperlink = ({
element, element,
elementsMap,
setAppState, setAppState,
onLinkOpen, onLinkOpen,
setToast, setToast,
updateEmbedValidationStatus, updateEmbedValidationStatus,
}: { }: {
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
elementsMap: ElementsMap;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
onLinkOpen: ExcalidrawProps["onLinkOpen"]; onLinkOpen: ExcalidrawProps["onLinkOpen"];
setToast: ( setToast: (
@ -182,7 +185,7 @@ export const Hyperlink = ({
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
const shouldHide = shouldHideLinkPopup(element, appState, [ const shouldHide = shouldHideLinkPopup(element, elementsMap, appState, [
event.clientX, event.clientX,
event.clientY, event.clientY,
]) as boolean; ]) as boolean;
@ -199,7 +202,7 @@ export const Hyperlink = ({
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
}; };
}, [appState, element, isEditing, setAppState]); }, [appState, element, isEditing, setAppState, elementsMap]);
const handleRemove = useCallback(() => { const handleRemove = useCallback(() => {
trackEvent("hyperlink", "delete"); trackEvent("hyperlink", "delete");
@ -214,7 +217,7 @@ export const Hyperlink = ({
trackEvent("hyperlink", "edit", "popup-ui"); trackEvent("hyperlink", "edit", "popup-ui");
setAppState({ showHyperlinkPopup: "editor" }); setAppState({ showHyperlinkPopup: "editor" });
}; };
const { x, y } = getCoordsForPopover(element, appState); const { x, y } = getCoordsForPopover(element, appState, elementsMap);
if ( if (
appState.contextMenu || appState.contextMenu ||
appState.draggingElement || appState.draggingElement ||
@ -324,8 +327,9 @@ export const Hyperlink = ({
const getCoordsForPopover = ( const getCoordsForPopover = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1] = getElementAbsoluteCoords(element); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width / 2, sceneY: y1 }, { sceneX: x1 + element.width / 2, sceneY: y1 },
appState, appState,
@ -430,11 +434,12 @@ export const getLinkHandleFromCoords = (
export const isPointHittingLinkIcon = ( export const isPointHittingLinkIcon = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState, appState: AppState,
[x, y]: Point, [x, y]: Point,
) => { ) => {
const threshold = 4 / appState.zoom.value; 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( const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
[x1, y1, x2, y2], [x1, y1, x2, y2],
element.angle, element.angle,
@ -450,6 +455,7 @@ export const isPointHittingLinkIcon = (
export const isPointHittingLink = ( export const isPointHittingLink = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState, appState: AppState,
[x, y]: Point, [x, y]: Point,
isMobile: boolean, isMobile: boolean,
@ -461,23 +467,30 @@ export const isPointHittingLink = (
if ( if (
!isMobile && !isMobile &&
appState.viewModeEnabled && appState.viewModeEnabled &&
isPointHittingElementBoundingBox(element, [x, y], threshold, null) isPointHittingElementBoundingBox(
element,
elementsMap,
[x, y],
threshold,
null,
)
) { ) {
return true; return true;
} }
return isPointHittingLinkIcon(element, appState, [x, y]); return isPointHittingLinkIcon(element, elementsMap, appState, [x, y]);
}; };
let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null; let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
export const showHyperlinkTooltip = ( export const showHyperlinkTooltip = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) { if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID); clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
} }
HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout( HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout(
() => renderTooltip(element, appState), () => renderTooltip(element, appState, elementsMap),
HYPERLINK_TOOLTIP_DELAY, HYPERLINK_TOOLTIP_DELAY,
); );
}; };
@ -485,6 +498,7 @@ export const showHyperlinkTooltip = (
const renderTooltip = ( const renderTooltip = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
if (!element.link) { if (!element.link) {
return; return;
@ -496,7 +510,7 @@ const renderTooltip = (
tooltipDiv.style.maxWidth = "20rem"; tooltipDiv.style.maxWidth = "20rem";
tooltipDiv.textContent = element.link; 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( const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
[x1, y1, x2, y2], [x1, y1, x2, y2],
@ -535,6 +549,7 @@ export const hideHyperlinkToolip = () => {
export const shouldHideLinkPopup = ( export const shouldHideLinkPopup = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState, appState: AppState,
[clientX, clientY]: Point, [clientX, clientY]: Point,
): Boolean => { ): Boolean => {
@ -546,11 +561,17 @@ export const shouldHideLinkPopup = (
const threshold = 15 / appState.zoom.value; const threshold = 15 / appState.zoom.value;
// hitbox to prevent hiding when hovered in element bounding box // hitbox to prevent hiding when hovered in element bounding box
if ( if (
isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold, null) isPointHittingElementBoundingBox(
element,
elementsMap,
[sceneX, sceneY],
threshold,
null,
)
) { ) {
return false; 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 // hit box to prevent hiding when hovered in the vertical area between element and popover
if ( if (
sceneX >= x1 && sceneX >= x1 &&
@ -561,7 +582,11 @@ export const shouldHideLinkPopup = (
return false; return false;
} }
// hit box to prevent hiding when hovered around popover within threshold // 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 ( if (
clientX >= popoverX - threshold && clientX >= popoverX - threshold &&

View file

@ -5,6 +5,7 @@ import {
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
PointBinding, PointBinding,
ExcalidrawElement, ExcalidrawElement,
ElementsMap,
} from "./types"; } from "./types";
import { getElementAtPosition } from "../scene"; import { getElementAtPosition } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
@ -66,6 +67,7 @@ export const bindOrUnbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startBindingElement: ExcalidrawBindableElement | null | "keep", startBindingElement: ExcalidrawBindableElement | null | "keep",
endBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep",
elementsMap: ElementsMap,
): void => { ): void => {
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set(); const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set(); const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
@ -76,6 +78,7 @@ export const bindOrUnbindLinearElement = (
"start", "start",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap,
); );
bindOrUnbindLinearElementEdge( bindOrUnbindLinearElementEdge(
linearElement, linearElement,
@ -84,6 +87,7 @@ export const bindOrUnbindLinearElement = (
"end", "end",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap,
); );
const onlyUnbound = Array.from(unboundFromElementIds).filter( const onlyUnbound = Array.from(unboundFromElementIds).filter(
@ -111,6 +115,7 @@ const bindOrUnbindLinearElementEdge = (
boundToElementIds: Set<ExcalidrawBindableElement["id"]>, boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
// Is mutated // Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>, unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: ElementsMap,
): void => { ): void => {
if (bindableElement !== "keep") { if (bindableElement !== "keep") {
if (bindableElement != null) { if (bindableElement != null) {
@ -127,7 +132,12 @@ const bindOrUnbindLinearElementEdge = (
: startOrEnd === "start" || : startOrEnd === "start" ||
otherEdgeBindableElement.id !== bindableElement.id) otherEdgeBindableElement.id !== bindableElement.id)
) { ) {
bindLinearElement(linearElement, bindableElement, startOrEnd); bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
elementsMap,
);
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
} else { } else {
@ -140,23 +150,34 @@ const bindOrUnbindLinearElementEdge = (
}; };
export const bindOrUnbindSelectedElements = ( export const bindOrUnbindSelectedElements = (
elements: NonDeleted<ExcalidrawElement>[], selectedElements: NonDeleted<ExcalidrawElement>[],
elementsMap: ElementsMap,
): void => { ): void => {
elements.forEach((element) => { selectedElements.forEach((selectedElement) => {
if (isBindingElement(element)) { if (isBindingElement(selectedElement)) {
bindOrUnbindLinearElement( bindOrUnbindLinearElement(
element, selectedElement,
getElligibleElementForBindingElement(element, "start"), getElligibleElementForBindingElement(
getElligibleElementForBindingElement(element, "end"), selectedElement,
"start",
elementsMap,
),
getElligibleElementForBindingElement(
selectedElement,
"end",
elementsMap,
),
elementsMap,
); );
} else if (isBindableElement(element)) { } else if (isBindableElement(selectedElement)) {
maybeBindBindableElement(element); maybeBindBindableElement(selectedElement, elementsMap);
} }
}); });
}; };
const maybeBindBindableElement = ( const maybeBindBindableElement = (
bindableElement: NonDeleted<ExcalidrawBindableElement>, bindableElement: NonDeleted<ExcalidrawBindableElement>,
elementsMap: ElementsMap,
): void => { ): void => {
getElligibleElementsForBindableElementAndWhere(bindableElement).forEach( getElligibleElementsForBindableElementAndWhere(bindableElement).forEach(
([linearElement, where]) => ([linearElement, where]) =>
@ -164,6 +185,7 @@ const maybeBindBindableElement = (
linearElement, linearElement,
where === "end" ? "keep" : bindableElement, where === "end" ? "keep" : bindableElement,
where === "start" ? "keep" : bindableElement, where === "start" ? "keep" : bindableElement,
elementsMap,
), ),
); );
}; };
@ -173,9 +195,15 @@ export const maybeBindLinearElement = (
appState: AppState, appState: AppState,
scene: Scene, scene: Scene,
pointerCoords: { x: number; y: number }, pointerCoords: { x: number; y: number },
elementsMap: ElementsMap,
): void => { ): void => {
if (appState.startBoundElement != null) { if (appState.startBoundElement != null) {
bindLinearElement(linearElement, appState.startBoundElement, "start"); bindLinearElement(
linearElement,
appState.startBoundElement,
"start",
elementsMap,
);
} }
const hoveredElement = getHoveredElementForBinding(pointerCoords, scene); const hoveredElement = getHoveredElementForBinding(pointerCoords, scene);
if ( if (
@ -186,7 +214,7 @@ export const maybeBindLinearElement = (
"end", "end",
) )
) { ) {
bindLinearElement(linearElement, hoveredElement, "end"); bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
} }
}; };
@ -194,11 +222,17 @@ export const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap,
): void => { ): void => {
mutateElement(linearElement, { mutateElement(linearElement, {
[startOrEnd === "start" ? "startBinding" : "endBinding"]: { [startOrEnd === "start" ? "startBinding" : "endBinding"]: {
elementId: hoveredElement.id, elementId: hoveredElement.id,
...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd), ...calculateFocusAndGap(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
} as PointBinding, } as PointBinding,
}); });
@ -240,10 +274,11 @@ export const isLinearElementSimpleAndAlreadyBound = (
export const unbindLinearElements = ( export const unbindLinearElements = (
elements: NonDeleted<ExcalidrawElement>[], elements: NonDeleted<ExcalidrawElement>[],
elementsMap: ElementsMap,
): void => { ): void => {
elements.forEach((element) => { elements.forEach((element) => {
if (isBindingElement(element)) { if (isBindingElement(element)) {
bindOrUnbindLinearElement(element, null, null); bindOrUnbindLinearElement(element, null, null, elementsMap);
} }
}); });
}; };
@ -272,7 +307,11 @@ export const getHoveredElementForBinding = (
scene.getNonDeletedElements(), scene.getNonDeletedElements(),
(element) => (element) =>
isBindableElement(element, false) && isBindableElement(element, false) &&
bindingBorderTest(element, pointerCoords), bindingBorderTest(
element,
pointerCoords,
scene.getNonDeletedElementsMap(),
),
); );
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null; return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
}; };
@ -281,21 +320,33 @@ const calculateFocusAndGap = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap,
): { focus: number; gap: number } => { ): { focus: number; gap: number } => {
const direction = startOrEnd === "start" ? -1 : 1; const direction = startOrEnd === "start" ? -1 : 1;
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1; const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
const adjacentPointIndex = edgePointIndex - direction; const adjacentPointIndex = edgePointIndex - direction;
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement, linearElement,
edgePointIndex, edgePointIndex,
elementsMap,
); );
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement, linearElement,
adjacentPointIndex, adjacentPointIndex,
elementsMap,
); );
return { return {
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), focus: determineFocusDistance(
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)), hoveredElement,
adjacentPoint,
edgePoint,
elementsMap,
),
gap: Math.max(
1,
distanceToBindableElement(hoveredElement, edgePoint, elementsMap),
),
}; };
}; };
@ -306,6 +357,8 @@ const calculateFocusAndGap = (
// in explicitly. // in explicitly.
export const updateBoundElements = ( export const updateBoundElements = (
changedElement: NonDeletedExcalidrawElement, changedElement: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
@ -355,12 +408,14 @@ export const updateBoundElements = (
"start", "start",
startBinding, startBinding,
changedElement as ExcalidrawBindableElement, changedElement as ExcalidrawBindableElement,
elementsMap,
); );
updateBoundPoint( updateBoundPoint(
element, element,
"end", "end",
endBinding, endBinding,
changedElement as ExcalidrawBindableElement, changedElement as ExcalidrawBindableElement,
elementsMap,
); );
const boundText = getBoundTextElement( const boundText = getBoundTextElement(
element, element,
@ -393,6 +448,7 @@ const updateBoundPoint = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
binding: PointBinding | null | undefined, binding: PointBinding | null | undefined,
changedElement: ExcalidrawBindableElement, changedElement: ExcalidrawBindableElement,
elementsMap: ElementsMap,
): void => { ): void => {
if ( if (
binding == null || binding == null ||
@ -414,11 +470,13 @@ const updateBoundPoint = (
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement, linearElement,
adjacentPointIndex, adjacentPointIndex,
elementsMap,
); );
const focusPointAbsolute = determineFocusPoint( const focusPointAbsolute = determineFocusPoint(
bindingElement, bindingElement,
binding.focus, binding.focus,
adjacentPoint, adjacentPoint,
elementsMap,
); );
let newEdgePoint; let newEdgePoint;
// The linear element was not originally pointing inside the bound shape, // The linear element was not originally pointing inside the bound shape,
@ -431,6 +489,7 @@ const updateBoundPoint = (
adjacentPoint, adjacentPoint,
focusPointAbsolute, focusPointAbsolute,
binding.gap, binding.gap,
elementsMap,
); );
if (intersections.length === 0) { if (intersections.length === 0) {
// This should never happen, since focusPoint should always be // This should never happen, since focusPoint should always be
@ -449,6 +508,7 @@ const updateBoundPoint = (
point: LinearElementEditor.pointFromAbsoluteCoords( point: LinearElementEditor.pointFromAbsoluteCoords(
linearElement, linearElement,
newEdgePoint, newEdgePoint,
elementsMap,
), ),
}, },
], ],
@ -480,12 +540,14 @@ const maybeCalculateNewGapWhenScaling = (
// TODO: this is a bottleneck, optimise // TODO: this is a bottleneck, optimise
export const getEligibleElementsForBinding = ( export const getEligibleElementsForBinding = (
elements: NonDeleted<ExcalidrawElement>[], elements: NonDeleted<ExcalidrawElement>[],
elementsMap: ElementsMap,
): SuggestedBinding[] => { ): SuggestedBinding[] => {
const includedElementIds = new Set(elements.map(({ id }) => id)); const includedElementIds = new Set(elements.map(({ id }) => id));
return elements.flatMap((element) => return elements.flatMap((element) =>
isBindingElement(element, false) isBindingElement(element, false)
? (getElligibleElementsForBindingElement( ? (getElligibleElementsForBindingElement(
element as NonDeleted<ExcalidrawLinearElement>, element as NonDeleted<ExcalidrawLinearElement>,
elementsMap,
).filter( ).filter(
(element) => !includedElementIds.has(element.id), (element) => !includedElementIds.has(element.id),
) as SuggestedBinding[]) ) as SuggestedBinding[])
@ -499,10 +561,11 @@ export const getEligibleElementsForBinding = (
const getElligibleElementsForBindingElement = ( const getElligibleElementsForBindingElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
): NonDeleted<ExcalidrawBindableElement>[] => { ): NonDeleted<ExcalidrawBindableElement>[] => {
return [ return [
getElligibleElementForBindingElement(linearElement, "start"), getElligibleElementForBindingElement(linearElement, "start", elementsMap),
getElligibleElementForBindingElement(linearElement, "end"), getElligibleElementForBindingElement(linearElement, "end", elementsMap),
].filter( ].filter(
(element): element is NonDeleted<ExcalidrawBindableElement> => (element): element is NonDeleted<ExcalidrawBindableElement> =>
element != null, element != null,
@ -512,9 +575,10 @@ const getElligibleElementsForBindingElement = (
const getElligibleElementForBindingElement = ( const getElligibleElementForBindingElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap,
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement> | null => {
return getHoveredElementForBinding( return getHoveredElementForBinding(
getLinearElementEdgeCoors(linearElement, startOrEnd), getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
Scene.getScene(linearElement)!, Scene.getScene(linearElement)!,
); );
}; };
@ -522,17 +586,23 @@ const getElligibleElementForBindingElement = (
const getLinearElementEdgeCoors = ( const getLinearElementEdgeCoors = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap,
): { x: number; y: number } => { ): { x: number; y: number } => {
const index = startOrEnd === "start" ? 0 : -1; const index = startOrEnd === "start" ? 0 : -1;
return tupleToCoors( return tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(linearElement, index), LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
index,
elementsMap,
),
); );
}; };
const getElligibleElementsForBindableElementAndWhere = ( const getElligibleElementsForBindableElementAndWhere = (
bindableElement: NonDeleted<ExcalidrawBindableElement>, bindableElement: NonDeleted<ExcalidrawBindableElement>,
): SuggestedPointBinding[] => { ): SuggestedPointBinding[] => {
return Scene.getScene(bindableElement)! const scene = Scene.getScene(bindableElement)!;
return scene
.getNonDeletedElements() .getNonDeletedElements()
.map((element) => { .map((element) => {
if (!isBindingElement(element, false)) { if (!isBindingElement(element, false)) {
@ -542,11 +612,13 @@ const getElligibleElementsForBindableElementAndWhere = (
element, element,
"start", "start",
bindableElement, bindableElement,
scene.getNonDeletedElementsMap(),
); );
const canBindEnd = isLinearElementEligibleForNewBindingByBindable( const canBindEnd = isLinearElementEligibleForNewBindingByBindable(
element, element,
"end", "end",
bindableElement, bindableElement,
scene.getNonDeletedElementsMap(),
); );
if (!canBindStart && !canBindEnd) { if (!canBindStart && !canBindEnd) {
return null; return null;
@ -564,6 +636,7 @@ const isLinearElementEligibleForNewBindingByBindable = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
bindableElement: NonDeleted<ExcalidrawBindableElement>, bindableElement: NonDeleted<ExcalidrawBindableElement>,
elementsMap: ElementsMap,
): boolean => { ): boolean => {
const existingBinding = const existingBinding =
linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"]; linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];
@ -576,7 +649,8 @@ const isLinearElementEligibleForNewBindingByBindable = (
) && ) &&
bindingBorderTest( bindingBorderTest(
bindableElement, bindableElement,
getLinearElementEdgeCoors(linearElement, startOrEnd), getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
elementsMap,
) )
); );
}; };

View file

@ -1,4 +1,5 @@
import { ROUNDNESS } from "../constants"; import { ROUNDNESS } from "../constants";
import { arrayToMap } from "../utils";
import { getElementAbsoluteCoords, getElementBounds } from "./bounds"; import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types"; import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
@ -35,26 +36,26 @@ const _ce = ({
describe("getElementAbsoluteCoords", () => { describe("getElementAbsoluteCoords", () => {
it("test x1 coordinate", () => { 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); expect(x1).toEqual(10);
}); });
it("test x2 coordinate", () => { it("test x2 coordinate", () => {
const [, , x2] = getElementAbsoluteCoords( const element = _ce({ x: 10, y: 20, w: 10, h: 0 });
_ce({ x: 10, y: 0, w: 10, h: 0 }), const [, , x2] = getElementAbsoluteCoords(element, arrayToMap([element]));
);
expect(x2).toEqual(20); expect(x2).toEqual(20);
}); });
it("test y1 coordinate", () => { 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); expect(y1).toEqual(10);
}); });
it("test y2 coordinate", () => { it("test y2 coordinate", () => {
const [, , , y2] = getElementAbsoluteCoords( const element = _ce({ x: 0, y: 10, w: 0, h: 10 });
_ce({ x: 0, y: 10, w: 0, h: 10 }), const [, , , y2] = getElementAbsoluteCoords(element, arrayToMap([element]));
);
expect(y2).toEqual(20); expect(y2).toEqual(20);
}); });
}); });

View file

@ -102,8 +102,10 @@ export class ElementBounds {
): Bounds { ): Bounds {
let bounds: 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)) { if (isFreeDrawElement(element)) {
const [minX, minY, maxX, maxY] = getBoundsFromPoints( const [minX, minY, maxX, maxY] = getBoundsFromPoints(
element.points.map(([x, y]) => element.points.map(([x, y]) =>
@ -159,10 +161,9 @@ export class ElementBounds {
// This set of functions retrieves the absolute position of the 4 points. // This set of functions retrieves the absolute position of the 4 points.
export const getElementAbsoluteCoords = ( export const getElementAbsoluteCoords = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
includeBoundText: boolean = false, includeBoundText: boolean = false,
): [number, number, number, number, number, number] => { ): [number, number, number, number, number, number] => {
const elementsMap =
Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
if (isFreeDrawElement(element)) { if (isFreeDrawElement(element)) {
return getFreeDrawElementAbsoluteCoords(element); return getFreeDrawElementAbsoluteCoords(element);
} else if (isLinearElement(element)) { } else if (isLinearElement(element)) {
@ -179,6 +180,7 @@ export const getElementAbsoluteCoords = (
const coords = LinearElementEditor.getBoundTextElementPosition( const coords = LinearElementEditor.getBoundTextElementPosition(
container, container,
element as ExcalidrawTextElementWithContainer, element as ExcalidrawTextElementWithContainer,
elementsMap,
); );
return [ return [
coords.x, coords.x,
@ -207,8 +209,12 @@ export const getElementAbsoluteCoords = (
*/ */
export const getElementLineSegments = ( export const getElementLineSegments = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
): [Point, Point][] => { ): [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]; const center: Point = [cx, cy];
@ -703,6 +709,7 @@ const getLinearElementRotatedBounds = (
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap,
[x, y, x, y], [x, y, x, y],
boundTextElement, boundTextElement,
); );
@ -727,6 +734,7 @@ const getLinearElementRotatedBounds = (
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap,
coords, coords,
boundTextElement, boundTextElement,
); );

View file

@ -91,6 +91,7 @@ export const hitTest = (
) { ) {
return isPointHittingElementBoundingBox( return isPointHittingElementBoundingBox(
element, element,
elementsMap,
point, point,
threshold, threshold,
frameNameBoundsCache, frameNameBoundsCache,
@ -116,6 +117,7 @@ export const hitTest = (
appState, appState,
frameNameBoundsCache, frameNameBoundsCache,
point, point,
elementsMap,
); );
}; };
@ -145,9 +147,11 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
appState, appState,
frameNameBoundsCache, frameNameBoundsCache,
[x, y], [x, y],
elementsMap,
) && ) &&
isPointHittingElementBoundingBox( isPointHittingElementBoundingBox(
element, element,
elementsMap,
[x, y], [x, y],
threshold, threshold,
frameNameBoundsCache, frameNameBoundsCache,
@ -160,6 +164,7 @@ export const isHittingElementNotConsideringBoundingBox = (
appState: AppState, appState: AppState,
frameNameBoundsCache: FrameNameBoundsCache | null, frameNameBoundsCache: FrameNameBoundsCache | null,
point: Point, point: Point,
elementsMap: ElementsMap,
): boolean => { ): boolean => {
const threshold = 10 / appState.zoom.value; const threshold = 10 / appState.zoom.value;
const check = isTextElement(element) const check = isTextElement(element)
@ -169,6 +174,7 @@ export const isHittingElementNotConsideringBoundingBox = (
: isNearCheck; : isNearCheck;
return hitTestPointAgainstElement({ return hitTestPointAgainstElement({
element, element,
elementsMap,
point, point,
threshold, threshold,
check, check,
@ -183,6 +189,7 @@ const isElementSelected = (
export const isPointHittingElementBoundingBox = ( export const isPointHittingElementBoundingBox = (
element: NonDeleted<ExcalidrawElement>, element: NonDeleted<ExcalidrawElement>,
elementsMap: ElementsMap,
[x, y]: Point, [x, y]: Point,
threshold: number, threshold: number,
frameNameBoundsCache: FrameNameBoundsCache | null, frameNameBoundsCache: FrameNameBoundsCache | null,
@ -194,6 +201,7 @@ export const isPointHittingElementBoundingBox = (
if (isFrameLikeElement(element)) { if (isFrameLikeElement(element)) {
return hitTestPointAgainstElement({ return hitTestPointAgainstElement({
element, element,
elementsMap,
point: [x, y], point: [x, y],
threshold, threshold,
check: isInsideCheck, 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 elementCenterX = (x1 + x2) / 2;
const elementCenterY = (y1 + y2) / 2; const elementCenterY = (y1 + y2) / 2;
// reverse rotate to take element's angle into account. // reverse rotate to take element's angle into account.
@ -224,12 +232,14 @@ export const isPointHittingElementBoundingBox = (
export const bindingBorderTest = ( export const bindingBorderTest = (
element: NonDeleted<ExcalidrawBindableElement>, element: NonDeleted<ExcalidrawBindableElement>,
{ x, y }: { x: number; y: number }, { x, y }: { x: number; y: number },
elementsMap: ElementsMap,
): boolean => { ): boolean => {
const threshold = maxBindingGap(element, element.width, element.height); const threshold = maxBindingGap(element, element.width, element.height);
const check = isOutsideCheck; const check = isOutsideCheck;
const point: Point = [x, y]; const point: Point = [x, y];
return hitTestPointAgainstElement({ return hitTestPointAgainstElement({
element, element,
elementsMap,
point, point,
threshold, threshold,
check, check,
@ -251,6 +261,7 @@ export const maxBindingGap = (
type HitTestArgs = { type HitTestArgs = {
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
elementsMap: ElementsMap;
point: Point; point: Point;
threshold: number; threshold: number;
check: (distance: number, threshold: number) => boolean; check: (distance: number, threshold: number) => boolean;
@ -266,19 +277,28 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
case "text": case "text":
case "diamond": case "diamond":
case "ellipse": case "ellipse":
const distance = distanceToBindableElement(args.element, args.point); const distance = distanceToBindableElement(
args.element,
args.point,
args.elementsMap,
);
return args.check(distance, args.threshold); return args.check(distance, args.threshold);
case "freedraw": { case "freedraw": {
if ( if (
!args.check( !args.check(
distanceToRectangle(args.element, args.point), distanceToRectangle(args.element, args.point, args.elementsMap),
args.threshold, args.threshold,
) )
) { ) {
return false; return false;
} }
return hitTestFreeDrawElement(args.element, args.point, args.threshold); return hitTestFreeDrawElement(
args.element,
args.point,
args.threshold,
args.elementsMap,
);
} }
case "arrow": case "arrow":
case "line": case "line":
@ -293,7 +313,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
// check distance to frame element first // check distance to frame element first
if ( if (
args.check( args.check(
distanceToBindableElement(args.element, args.point), distanceToBindableElement(args.element, args.point, args.elementsMap),
args.threshold, args.threshold,
) )
) { ) {
@ -316,6 +336,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
export const distanceToBindableElement = ( export const distanceToBindableElement = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
switch (element.type) { switch (element.type) {
case "rectangle": case "rectangle":
@ -325,11 +346,11 @@ export const distanceToBindableElement = (
case "embeddable": case "embeddable":
case "frame": case "frame":
case "magicframe": case "magicframe":
return distanceToRectangle(element, point); return distanceToRectangle(element, point, elementsMap);
case "diamond": case "diamond":
return distanceToDiamond(element, point); return distanceToDiamond(element, point, elementsMap);
case "ellipse": case "ellipse":
return distanceToEllipse(element, point); return distanceToEllipse(element, point, elementsMap);
} }
}; };
@ -358,8 +379,13 @@ const distanceToRectangle = (
| ExcalidrawIframeLikeElement | ExcalidrawIframeLikeElement
| ExcalidrawFrameLikeElement, | ExcalidrawFrameLikeElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); const [, pointRel, hwidth, hheight] = pointRelativeToElement(
element,
point,
elementsMap,
);
return Math.max( return Math.max(
GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)), GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)),
GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)), GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)),
@ -377,8 +403,13 @@ const distanceToRectangleBox = (box: RectangleBox, point: Point): number => {
const distanceToDiamond = ( const distanceToDiamond = (
element: ExcalidrawDiamondElement, element: ExcalidrawDiamondElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); const [, pointRel, hwidth, hheight] = pointRelativeToElement(
element,
point,
elementsMap,
);
const side = GALine.equation(hheight, hwidth, -hheight * hwidth); const side = GALine.equation(hheight, hwidth, -hheight * hwidth);
return GAPoint.distanceToLine(pointRel, side); return GAPoint.distanceToLine(pointRel, side);
}; };
@ -386,16 +417,22 @@ const distanceToDiamond = (
const distanceToEllipse = ( const distanceToEllipse = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const [pointRel, tangent] = ellipseParamsForTest(element, point); const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent); return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent);
}; };
const ellipseParamsForTest = ( const ellipseParamsForTest = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): [GA.Point, GA.Line] => { ): [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); const [px, py] = GAPoint.toTuple(pointRel);
// We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)` // We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)`
@ -440,6 +477,7 @@ const hitTestFreeDrawElement = (
element: ExcalidrawFreeDrawElement, element: ExcalidrawFreeDrawElement,
point: Point, point: Point,
threshold: number, threshold: number,
elementsMap: ElementsMap,
): boolean => { ): boolean => {
// Check point-distance-to-line-segment for every segment in the // Check point-distance-to-line-segment for every segment in the
// element's points (its input points, not its outline points). // element's points (its input points, not its outline points).
@ -454,7 +492,10 @@ const hitTestFreeDrawElement = (
y = point[1] - element.y; y = point[1] - element.y;
} else { } else {
// Counter-rotate the point around center before testing // 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( const rotatedPoint = rotatePoint(
point, point,
[minX + (maxX - minX) / 2, minY + (maxY - minY) / 2], [minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
@ -520,6 +561,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
const [point, pointAbs, hwidth, hheight] = pointRelativeToElement( const [point, pointAbs, hwidth, hheight] = pointRelativeToElement(
args.element, args.element,
args.point, args.point,
args.elementsMap,
); );
const side1 = GALine.equation(0, 1, -hheight); const side1 = GALine.equation(0, 1, -hheight);
const side2 = GALine.equation(1, 0, -hwidth); const side2 = GALine.equation(1, 0, -hwidth);
@ -572,9 +614,10 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
const pointRelativeToElement = ( const pointRelativeToElement = (
element: ExcalidrawElement, element: ExcalidrawElement,
pointTuple: Point, pointTuple: Point,
elementsMap: ElementsMap,
): [GA.Point, GA.Point, number, number] => { ): [GA.Point, GA.Point, number, number] => {
const point = GAPoint.from(pointTuple); 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); const center = coordsCenter(x1, y1, x2, y2);
// GA has angle orientation opposite to `rotate` // GA has angle orientation opposite to `rotate`
const rotate = GATransform.rotation(center, element.angle); const rotate = GATransform.rotation(center, element.angle);
@ -609,11 +652,12 @@ const pointRelativeToDivElement = (
// Returns point in absolute coordinates // Returns point in absolute coordinates
export const pointInAbsoluteCoords = ( export const pointInAbsoluteCoords = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
// Point relative to the element position // Point relative to the element position
point: Point, point: Point,
): Point => { ): Point => {
const [x, y] = 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 cx = (x2 - x1) / 2;
const cy = (y2 - y1) / 2; const cy = (y2 - y1) / 2;
const [rotatedX, rotatedY] = rotate(x, y, cx, cy, element.angle); const [rotatedX, rotatedY] = rotate(x, y, cx, cy, element.angle);
@ -622,8 +666,9 @@ export const pointInAbsoluteCoords = (
const relativizationToElementCenter = ( const relativizationToElementCenter = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
): GA.Transform => { ): GA.Transform => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const center = coordsCenter(x1, y1, x2, y2); const center = coordsCenter(x1, y1, x2, y2);
// GA has angle orientation opposite to `rotate` // GA has angle orientation opposite to `rotate`
const rotate = GATransform.rotation(center, element.angle); const rotate = GATransform.rotation(center, element.angle);
@ -649,12 +694,14 @@ const coordsCenter = (
// of the element. // of the element.
export const determineFocusDistance = ( export const determineFocusDistance = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
// Point on the line, in absolute coordinates // Point on the line, in absolute coordinates
a: Point, a: Point,
// Another point on the line, in absolute coordinates (closer to element) // Another point on the line, in absolute coordinates (closer to element)
b: Point, b: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const relateToCenter = relativizationToElementCenter(element); const relateToCenter = relativizationToElementCenter(element, elementsMap);
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a)); const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b)); const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
const line = GALine.through(aRel, bRel); const line = GALine.through(aRel, bRel);
@ -693,13 +740,14 @@ export const determineFocusPoint = (
// returned focusPoint // returned focusPoint
focus: number, focus: number,
adjecentPoint: Point, adjecentPoint: Point,
elementsMap: ElementsMap,
): Point => { ): Point => {
if (focus === 0) { 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); const center = coordsCenter(x1, y1, x2, y2);
return GAPoint.toTuple(center); return GAPoint.toTuple(center);
} }
const relateToCenter = relativizationToElementCenter(element); const relateToCenter = relativizationToElementCenter(element, elementsMap);
const adjecentPointRel = GATransform.apply( const adjecentPointRel = GATransform.apply(
relateToCenter, relateToCenter,
GAPoint.from(adjecentPoint), GAPoint.from(adjecentPoint),
@ -728,14 +776,16 @@ export const determineFocusPoint = (
// and the `element`, in ascending order of distance from `a`. // and the `element`, in ascending order of distance from `a`.
export const intersectElementWithLine = ( export const intersectElementWithLine = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
// Point on the line, in absolute coordinates // Point on the line, in absolute coordinates
a: Point, a: Point,
// Another point on the line, in absolute coordinates // Another point on the line, in absolute coordinates
b: Point, b: Point,
// If given, the element is inflated by this value // If given, the element is inflated by this value
gap: number = 0, gap: number = 0,
elementsMap: ElementsMap,
): Point[] => { ): Point[] => {
const relateToCenter = relativizationToElementCenter(element); const relateToCenter = relativizationToElementCenter(element, elementsMap);
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a)); const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b)); const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
const line = GALine.through(aRel, bRel); const line = GALine.through(aRel, bRel);

View file

@ -65,7 +65,7 @@ export const dragSelectedElements = (
updateElementCoords(pointerDownState, textElement, adjustedOffset); updateElementCoords(pointerDownState, textElement, adjustedOffset);
} }
} }
updateBoundElements(element, { updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
simultaneouslyUpdated: Array.from(elementsToUpdate), simultaneouslyUpdated: Array.from(elementsToUpdate),
}); });
}); });

View file

@ -135,6 +135,7 @@ export class LinearElementEditor {
event: PointerEvent, event: PointerEvent,
appState: AppState, appState: AppState,
setState: React.Component<any, AppState>["setState"], setState: React.Component<any, AppState>["setState"],
elementsMap: ElementsMap,
) { ) {
if ( if (
!appState.editingLinearElement || !appState.editingLinearElement ||
@ -151,10 +152,12 @@ export class LinearElementEditor {
} }
const [selectionX1, selectionY1, selectionX2, selectionY2] = const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(appState.draggingElement); getElementAbsoluteCoords(appState.draggingElement, elementsMap);
const pointsSceneCoords = const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(
LinearElementEditor.getPointsGlobalCoordinates(element); element,
elementsMap,
);
const nextSelectedPoints = pointsSceneCoords.reduce( const nextSelectedPoints = pointsSceneCoords.reduce(
(acc: number[], point, index) => { (acc: number[], point, index) => {
@ -222,6 +225,7 @@ export class LinearElementEditor {
const [width, height] = LinearElementEditor._getShiftLockedDelta( const [width, height] = LinearElementEditor._getShiftLockedDelta(
element, element,
elementsMap,
referencePoint, referencePoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@ -239,6 +243,7 @@ export class LinearElementEditor {
} else { } else {
const newDraggingPointPosition = LinearElementEditor.createPointAt( const newDraggingPointPosition = LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@ -255,6 +260,7 @@ export class LinearElementEditor {
linearElementEditor.pointerDownState.lastClickedPoint linearElementEditor.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt( ? LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@ -290,6 +296,7 @@ export class LinearElementEditor {
LinearElementEditor.getPointGlobalCoordinates( LinearElementEditor.getPointGlobalCoordinates(
element, element,
element.points[0], element.points[0],
elementsMap,
), ),
), ),
); );
@ -303,6 +310,7 @@ export class LinearElementEditor {
LinearElementEditor.getPointGlobalCoordinates( LinearElementEditor.getPointGlobalCoordinates(
element, element,
element.points[lastSelectedIndex], element.points[lastSelectedIndex],
elementsMap,
), ),
), ),
); );
@ -323,6 +331,7 @@ export class LinearElementEditor {
event: PointerEvent, event: PointerEvent,
editingLinearElement: LinearElementEditor, editingLinearElement: LinearElementEditor,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
): LinearElementEditor { ): LinearElementEditor {
const { elementId, selectedPointsIndices, isDragging, pointerDownState } = const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
editingLinearElement; editingLinearElement;
@ -364,6 +373,7 @@ export class LinearElementEditor {
LinearElementEditor.getPointAtIndexGlobalCoordinates( LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
selectedPoint!, selectedPoint!,
elementsMap,
), ),
), ),
Scene.getScene(element)!, Scene.getScene(element)!,
@ -425,15 +435,23 @@ export class LinearElementEditor {
) { ) {
return editorMidPointsCache.points; return editorMidPointsCache.points;
} }
LinearElementEditor.updateEditorMidPointsCache(element, appState); LinearElementEditor.updateEditorMidPointsCache(
element,
elementsMap,
appState,
);
return editorMidPointsCache.points!; return editorMidPointsCache.points!;
}; };
static updateEditorMidPointsCache = ( static updateEditorMidPointsCache = (
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
) => { ) => {
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
let index = 0; let index = 0;
const midpoints: (Point | null)[] = []; const midpoints: (Point | null)[] = [];
@ -455,6 +473,7 @@ export class LinearElementEditor {
points[index], points[index],
points[index + 1], points[index + 1],
index + 1, index + 1,
elementsMap,
); );
midpoints.push(segmentMidPoint); midpoints.push(segmentMidPoint);
index++; index++;
@ -477,6 +496,7 @@ export class LinearElementEditor {
} }
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element, element,
elementsMap,
appState.zoom, appState.zoom,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
@ -484,7 +504,10 @@ export class LinearElementEditor {
if (clickedPointIndex >= 0) { if (clickedPointIndex >= 0) {
return null; return null;
} }
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
if (points.length >= 3 && !appState.editingLinearElement) { if (points.length >= 3 && !appState.editingLinearElement) {
return null; return null;
} }
@ -550,6 +573,7 @@ export class LinearElementEditor {
startPoint: Point, startPoint: Point,
endPoint: Point, endPoint: Point,
endPointIndex: number, endPointIndex: number,
elementsMap: ElementsMap,
) { ) {
let segmentMidPoint = centerPoint(startPoint, endPoint); let segmentMidPoint = centerPoint(startPoint, endPoint);
if (element.points.length > 2 && element.roundness) { if (element.points.length > 2 && element.roundness) {
@ -574,6 +598,7 @@ export class LinearElementEditor {
segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates( segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates(
element, element,
[tx, ty], [tx, ty],
elementsMap,
); );
} }
} }
@ -658,6 +683,7 @@ export class LinearElementEditor {
...element.points, ...element.points,
LinearElementEditor.createPointAt( LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@ -693,6 +719,7 @@ export class LinearElementEditor {
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element, element,
elementsMap,
appState.zoom, appState.zoom,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
@ -713,11 +740,12 @@ export class LinearElementEditor {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
); );
} }
} }
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const targetPoint = const targetPoint =
@ -779,6 +807,7 @@ export class LinearElementEditor {
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
): LinearElementEditor | null { ): LinearElementEditor | null {
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return null; return null;
@ -809,6 +838,7 @@ export class LinearElementEditor {
const [width, height] = LinearElementEditor._getShiftLockedDelta( const [width, height] = LinearElementEditor._getShiftLockedDelta(
element, element,
elementsMap,
lastCommittedPoint, lastCommittedPoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@ -821,6 +851,7 @@ export class LinearElementEditor {
} else { } else {
newPoint = LinearElementEditor.createPointAt( newPoint = LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointerX - appState.editingLinearElement.pointerOffset.x, scenePointerX - appState.editingLinearElement.pointerOffset.x,
scenePointerY - appState.editingLinearElement.pointerOffset.y, scenePointerY - appState.editingLinearElement.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@ -847,8 +878,9 @@ export class LinearElementEditor {
static getPointGlobalCoordinates( static getPointGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
point: Point, 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 cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
@ -860,8 +892,9 @@ export class LinearElementEditor {
/** scene coords */ /** scene coords */
static getPointsGlobalCoordinates( static getPointsGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
): Point[] { ): Point[] {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
return element.points.map((point) => { return element.points.map((point) => {
@ -873,13 +906,15 @@ export class LinearElementEditor {
static getPointAtIndexGlobalCoordinates( static getPointAtIndexGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
indexMaybeFromEnd: number, // -1 for last element indexMaybeFromEnd: number, // -1 for last element
elementsMap: ElementsMap,
): Point { ): Point {
const index = const index =
indexMaybeFromEnd < 0 indexMaybeFromEnd < 0
? element.points.length + indexMaybeFromEnd ? element.points.length + indexMaybeFromEnd
: indexMaybeFromEnd; : indexMaybeFromEnd;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
@ -893,8 +928,9 @@ export class LinearElementEditor {
static pointFromAbsoluteCoords( static pointFromAbsoluteCoords(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
absoluteCoords: Point, absoluteCoords: Point,
elementsMap: ElementsMap,
): Point { ): Point {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [x, y] = rotate( const [x, y] = rotate(
@ -909,12 +945,15 @@ export class LinearElementEditor {
static getPointIndexUnderCursor( static getPointIndexUnderCursor(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
zoom: AppState["zoom"], zoom: AppState["zoom"],
x: number, x: number,
y: number, y: number,
) { ) {
const pointHandles = const pointHandles = LinearElementEditor.getPointsGlobalCoordinates(
LinearElementEditor.getPointsGlobalCoordinates(element); element,
elementsMap,
);
let idx = pointHandles.length; let idx = pointHandles.length;
// loop from right to left because points on the right are rendered over // 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 // points on the left, thus should take precedence when clicking, if they
@ -934,12 +973,13 @@ export class LinearElementEditor {
static createPointAt( static createPointAt(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
gridSize: number | null, gridSize: number | null,
): Point { ): Point {
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize); 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 cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [rotatedX, rotatedY] = rotate( const [rotatedX, rotatedY] = rotate(
@ -1190,6 +1230,7 @@ export class LinearElementEditor {
pointerCoords: PointerCoords, pointerCoords: PointerCoords,
appState: AppState, appState: AppState,
snapToGrid: boolean, snapToGrid: boolean,
elementsMap: ElementsMap,
) { ) {
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
@ -1208,6 +1249,7 @@ export class LinearElementEditor {
const midpoint = LinearElementEditor.createPointAt( const midpoint = LinearElementEditor.createPointAt(
element, element,
elementsMap,
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
snapToGrid ? appState.gridSize : null, snapToGrid ? appState.gridSize : null,
@ -1260,6 +1302,7 @@ export class LinearElementEditor {
private static _getShiftLockedDelta( private static _getShiftLockedDelta(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
referencePoint: Point, referencePoint: Point,
scenePointer: Point, scenePointer: Point,
gridSize: number | null, gridSize: number | null,
@ -1267,6 +1310,7 @@ export class LinearElementEditor {
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
element, element,
referencePoint, referencePoint,
elementsMap,
); );
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
@ -1288,8 +1332,12 @@ export class LinearElementEditor {
static getBoundTextElementPosition = ( static getBoundTextElementPosition = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
elementsMap: ElementsMap,
): { x: number; y: number } => { ): { x: number; y: number } => {
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
if (points.length < 2) { if (points.length < 2) {
mutateElement(boundTextElement, { isDeleted: true }); mutateElement(boundTextElement, { isDeleted: true });
} }
@ -1300,6 +1348,7 @@ export class LinearElementEditor {
const midPoint = LinearElementEditor.getPointGlobalCoordinates( const midPoint = LinearElementEditor.getPointGlobalCoordinates(
element, element,
element.points[index], element.points[index],
elementsMap,
); );
x = midPoint[0] - boundTextElement.width / 2; x = midPoint[0] - boundTextElement.width / 2;
y = midPoint[1] - boundTextElement.height / 2; y = midPoint[1] - boundTextElement.height / 2;
@ -1319,6 +1368,7 @@ export class LinearElementEditor {
points[index], points[index],
points[index + 1], points[index + 1],
index + 1, index + 1,
elementsMap,
); );
} }
x = midSegmentMidpoint[0] - boundTextElement.width / 2; x = midSegmentMidpoint[0] - boundTextElement.width / 2;
@ -1329,6 +1379,7 @@ export class LinearElementEditor {
static getMinMaxXYWithBoundText = ( static getMinMaxXYWithBoundText = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
elementsMap: ElementsMap,
elementBounds: Bounds, elementBounds: Bounds,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
): [number, number, number, number, number, number] => { ): [number, number, number, number, number, number] => {
@ -1339,6 +1390,7 @@ export class LinearElementEditor {
LinearElementEditor.getBoundTextElementPosition( LinearElementEditor.getBoundTextElementPosition(
element, element,
boundTextElement, boundTextElement,
elementsMap,
); );
const boundTextX2 = boundTextX1 + boundTextElement.width; const boundTextX2 = boundTextX1 + boundTextElement.width;
const boundTextY2 = boundTextY1 + boundTextElement.height; const boundTextY2 = boundTextY1 + boundTextElement.height;
@ -1479,6 +1531,7 @@ export class LinearElementEditor {
if (boundTextElement) { if (boundTextElement) {
coords = LinearElementEditor.getMinMaxXYWithBoundText( coords = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap,
[x1, y1, x2, y2], [x1, y1, x2, y2],
boundTextElement, boundTextElement,
); );

View file

@ -16,6 +16,7 @@ import {
ExcalidrawEmbeddableElement, ExcalidrawEmbeddableElement,
ExcalidrawMagicFrameElement, ExcalidrawMagicFrameElement,
ExcalidrawIframeElement, ExcalidrawIframeElement,
ElementsMap,
} from "./types"; } from "./types";
import { import {
arrayToMap, arrayToMap,
@ -260,6 +261,7 @@ export const newTextElement = (
const getAdjustedDimensions = ( const getAdjustedDimensions = (
element: ExcalidrawTextElement, element: ExcalidrawTextElement,
elementsMap: ElementsMap,
nextText: string, nextText: string,
): { ): {
x: number; x: number;
@ -294,7 +296,7 @@ const getAdjustedDimensions = (
x = element.x - offsets.x; x = element.x - offsets.x;
y = element.y - offsets.y; y = element.y - offsets.y;
} else { } else {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords( const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
element, element,
@ -335,6 +337,7 @@ const getAdjustedDimensions = (
export const refreshTextDimensions = ( export const refreshTextDimensions = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null, container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
text = textElement.text, text = textElement.text,
) => { ) => {
if (textElement.isDeleted) { if (textElement.isDeleted) {
@ -347,13 +350,14 @@ export const refreshTextDimensions = (
getBoundTextMaxWidth(container, textElement), getBoundTextMaxWidth(container, textElement),
); );
} }
const dimensions = getAdjustedDimensions(textElement, text); const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
return { text, ...dimensions }; return { text, ...dimensions };
}; };
export const updateTextElement = ( export const updateTextElement = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null, container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
{ {
text, text,
isDeleted, isDeleted,
@ -367,7 +371,7 @@ export const updateTextElement = (
return newElementWith(textElement, { return newElementWith(textElement, {
originalText, originalText,
isDeleted: isDeleted ?? textElement.isDeleted, isDeleted: isDeleted ?? textElement.isDeleted,
...refreshTextDimensions(textElement, container, originalText), ...refreshTextDimensions(textElement, container, elementsMap, originalText),
}); });
}; };

View file

@ -86,11 +86,12 @@ export const transformElements = (
if (transformHandleType === "rotation") { if (transformHandleType === "rotation") {
rotateSingleElement( rotateSingleElement(
element, element,
elementsMap,
pointerX, pointerX,
pointerY, pointerY,
shouldRotateWithDiscreteAngle, shouldRotateWithDiscreteAngle,
); );
updateBoundElements(element); updateBoundElements(element, elementsMap);
} else if ( } else if (
isTextElement(element) && isTextElement(element) &&
(transformHandleType === "nw" || (transformHandleType === "nw" ||
@ -106,7 +107,7 @@ export const transformElements = (
pointerX, pointerX,
pointerY, pointerY,
); );
updateBoundElements(element); updateBoundElements(element, elementsMap);
} else if (transformHandleType) { } else if (transformHandleType) {
resizeSingleElement( resizeSingleElement(
originalElements, originalElements,
@ -157,11 +158,12 @@ export const transformElements = (
const rotateSingleElement = ( const rotateSingleElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
shouldRotateWithDiscreteAngle: boolean, shouldRotateWithDiscreteAngle: boolean,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
let angle: number; let angle: number;
@ -266,7 +268,7 @@ const resizeSingleTextElement = (
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
// rotation pointer with reverse angle // rotation pointer with reverse angle
@ -629,7 +631,7 @@ export const resizeSingleElement = (
) { ) {
mutateElement(element, resizedElement); mutateElement(element, resizedElement);
updateBoundElements(element, { updateBoundElements(element, elementsMap, {
newSize: { width: resizedElement.width, height: resizedElement.height }, newSize: { width: resizedElement.width, height: resizedElement.height },
}); });
@ -696,7 +698,11 @@ export const resizeMultipleElements = (
if (!isBoundToContainer(text)) { if (!isBoundToContainer(text)) {
return acc; return acc;
} }
const xy = LinearElementEditor.getBoundTextElementPosition(orig, text); const xy = LinearElementEditor.getBoundTextElementPosition(
orig,
text,
elementsMap,
);
return [...acc, { ...text, ...xy }]; return [...acc, { ...text, ...xy }];
}, [] as ExcalidrawTextElementWithContainer[]); }, [] as ExcalidrawTextElementWithContainer[]);
@ -879,7 +885,7 @@ export const resizeMultipleElements = (
mutateElement(element, update, false); mutateElement(element, update, false);
updateBoundElements(element, { updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elementsToUpdate, simultaneouslyUpdated: elementsToUpdate,
newSize: { width, height }, newSize: { width, height },
}); });
@ -921,7 +927,7 @@ const rotateMultipleElements = (
elements elements
.filter((element) => !isFrameLikeElement(element)) .filter((element) => !isFrameLikeElement(element))
.forEach((element) => { .forEach((element) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const origAngle = const origAngle =
@ -942,7 +948,9 @@ const rotateMultipleElements = (
}, },
false, false,
); );
updateBoundElements(element, { simultaneouslyUpdated: elements }); updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elements,
});
const boundText = getBoundTextElement(element, elementsMap); const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) { if (boundText && !isArrowElement(element)) {
@ -964,12 +972,13 @@ const rotateMultipleElements = (
export const getResizeOffsetXY = ( export const getResizeOffsetXY = (
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
x: number, x: number,
y: number, y: number,
): [number, number] => { ): [number, number] => {
const [x1, y1, x2, y2] = const [x1, y1, x2, y2] =
selectedElements.length === 1 selectedElements.length === 1
? getElementAbsoluteCoords(selectedElements[0]) ? getElementAbsoluteCoords(selectedElements[0], elementsMap)
: getCommonBounds(selectedElements); : getCommonBounds(selectedElements);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;

View file

@ -2,6 +2,7 @@ import {
ExcalidrawElement, ExcalidrawElement,
PointerType, PointerType,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
ElementsMap,
} from "./types"; } from "./types";
import { import {
@ -27,6 +28,7 @@ const isInsideTransformHandle = (
export const resizeTest = ( export const resizeTest = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState, appState: AppState,
x: number, x: number,
y: number, y: number,
@ -38,7 +40,7 @@ export const resizeTest = (
} }
const { rotation: rotationTransformHandle, ...transformHandles } = const { rotation: rotationTransformHandle, ...transformHandles } =
getTransformHandles(element, zoom, pointerType); getTransformHandles(element, zoom, elementsMap, pointerType);
if ( if (
rotationTransformHandle && rotationTransformHandle &&
@ -70,6 +72,7 @@ export const getElementWithTransformHandleType = (
scenePointerY: number, scenePointerY: number,
zoom: Zoom, zoom: Zoom,
pointerType: PointerType, pointerType: PointerType,
elementsMap: ElementsMap,
) => { ) => {
return elements.reduce((result, element) => { return elements.reduce((result, element) => {
if (result) { if (result) {
@ -77,6 +80,7 @@ export const getElementWithTransformHandleType = (
} }
const transformHandleType = resizeTest( const transformHandleType = resizeTest(
element, element,
elementsMap,
appState, appState,
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,

View file

@ -53,6 +53,7 @@ const splitIntoLines = (text: string) => {
export const redrawTextBoundingBox = ( export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null, container: ExcalidrawElement | null,
elementsMap: ElementsMap,
) => { ) => {
let maxWidth = undefined; let maxWidth = undefined;
const boundTextUpdates = { const boundTextUpdates = {
@ -110,7 +111,11 @@ export const redrawTextBoundingBox = (
...textElement, ...textElement,
...boundTextUpdates, ...boundTextUpdates,
} as ExcalidrawTextElementWithContainer; } as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(container, updatedTextElement); const { x, y } = computeBoundTextPosition(
container,
updatedTextElement,
elementsMap,
);
boundTextUpdates.x = x; boundTextUpdates.x = x;
boundTextUpdates.y = y; boundTextUpdates.y = y;
} }
@ -119,11 +124,11 @@ export const redrawTextBoundingBox = (
}; };
export const bindTextToShapeAfterDuplication = ( export const bindTextToShapeAfterDuplication = (
sceneElements: ExcalidrawElement[], newElements: ExcalidrawElement[],
oldElements: ExcalidrawElement[], oldElements: ExcalidrawElement[],
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>, oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
): void => { ): void => {
const sceneElementMap = arrayToMap(sceneElements) as Map< const newElementsMap = arrayToMap(newElements) as Map<
ExcalidrawElement["id"], ExcalidrawElement["id"],
ExcalidrawElement ExcalidrawElement
>; >;
@ -134,7 +139,7 @@ export const bindTextToShapeAfterDuplication = (
if (boundTextElementId) { if (boundTextElementId) {
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId); const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
if (newTextElementId) { if (newTextElementId) {
const newContainer = sceneElementMap.get(newElementId); const newContainer = newElementsMap.get(newElementId);
if (newContainer) { if (newContainer) {
mutateElement(newContainer, { mutateElement(newContainer, {
boundElements: (element.boundElements || []) boundElements: (element.boundElements || [])
@ -149,7 +154,7 @@ export const bindTextToShapeAfterDuplication = (
}), }),
}); });
} }
const newTextElement = sceneElementMap.get(newTextElementId); const newTextElement = newElementsMap.get(newTextElementId);
if (newTextElement && isTextElement(newTextElement)) { if (newTextElement && isTextElement(newTextElement)) {
mutateElement(newTextElement, { mutateElement(newTextElement, {
containerId: newContainer ? newElementId : null, containerId: newContainer ? newElementId : null,
@ -236,7 +241,7 @@ export const handleBindTextResize = (
if (!isArrowElement(container)) { if (!isArrowElement(container)) {
mutateElement( mutateElement(
textElement, textElement,
computeBoundTextPosition(container, textElement), computeBoundTextPosition(container, textElement, elementsMap),
); );
} }
} }
@ -245,11 +250,13 @@ export const handleBindTextResize = (
export const computeBoundTextPosition = ( export const computeBoundTextPosition = (
container: ExcalidrawElement, container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
elementsMap: ElementsMap,
) => { ) => {
if (isArrowElement(container)) { if (isArrowElement(container)) {
return LinearElementEditor.getBoundTextElementPosition( return LinearElementEditor.getBoundTextElementPosition(
container, container,
boundTextElement, boundTextElement,
elementsMap,
); );
} }
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
@ -698,12 +705,16 @@ export const getContainerCenter = (
y: container.y + container.height / 2, y: container.y + container.height / 2,
}; };
} }
const points = LinearElementEditor.getPointsGlobalCoordinates(container); const points = LinearElementEditor.getPointsGlobalCoordinates(
container,
elementsMap,
);
if (points.length % 2 === 1) { if (points.length % 2 === 1) {
const index = Math.floor(container.points.length / 2); const index = Math.floor(container.points.length / 2);
const midPoint = LinearElementEditor.getPointGlobalCoordinates( const midPoint = LinearElementEditor.getPointGlobalCoordinates(
container, container,
container.points[index], container.points[index],
elementsMap,
); );
return { x: midPoint[0], y: midPoint[1] }; return { x: midPoint[0], y: midPoint[1] };
} }
@ -719,6 +730,7 @@ export const getContainerCenter = (
points[index], points[index],
points[index + 1], points[index + 1],
index + 1, index + 1,
elementsMap,
); );
} }
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] }; return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
@ -757,11 +769,13 @@ export const getTextElementAngle = (
export const getBoundTextElementPosition = ( export const getBoundTextElementPosition = (
container: ExcalidrawElement, container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
elementsMap: ElementsMap,
) => { ) => {
if (isArrowElement(container)) { if (isArrowElement(container)) {
return LinearElementEditor.getBoundTextElementPosition( return LinearElementEditor.getBoundTextElementPosition(
container, container,
boundTextElement, boundTextElement,
elementsMap,
); );
} }
}; };
@ -804,6 +818,7 @@ export const getTextBindableContainerAtPosition = (
appState: AppState, appState: AppState,
x: number, x: number,
y: number, y: number,
elementsMap: ElementsMap,
): ExcalidrawTextContainer | null => { ): ExcalidrawTextContainer | null => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
@ -817,7 +832,10 @@ export const getTextBindableContainerAtPosition = (
if (elements[index].isDeleted) { if (elements[index].isDeleted) {
continue; continue;
} }
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]); const [x1, y1, x2, y2] = getElementAbsoluteCoords(
elements[index],
elementsMap,
);
if ( if (
isArrowElement(elements[index]) && isArrowElement(elements[index]) &&
isHittingElementNotConsideringBoundingBox( isHittingElementNotConsideringBoundingBox(
@ -825,6 +843,7 @@ export const getTextBindableContainerAtPosition = (
appState, appState,
null, null,
[x, y], [x, y],
elementsMap,
) )
) { ) {
hitElement = elements[index]; hitElement = elements[index];

View file

@ -121,13 +121,13 @@ export const textWysiwyg = ({
return; return;
} }
const { textAlign, verticalAlign } = updatedTextElement; const { textAlign, verticalAlign } = updatedTextElement;
const elementsMap = app.scene.getNonDeletedElementsMap();
if (updatedTextElement && isTextElement(updatedTextElement)) { if (updatedTextElement && isTextElement(updatedTextElement)) {
let coordX = updatedTextElement.x; let coordX = updatedTextElement.x;
let coordY = updatedTextElement.y; let coordY = updatedTextElement.y;
const container = getContainerElement( const container = getContainerElement(
updatedTextElement, updatedTextElement,
app.scene.getElementsMapIncludingDeleted(), app.scene.getNonDeletedElementsMap(),
); );
let maxWidth = updatedTextElement.width; let maxWidth = updatedTextElement.width;
@ -143,6 +143,7 @@ export const textWysiwyg = ({
LinearElementEditor.getBoundTextElementPosition( LinearElementEditor.getBoundTextElementPosition(
container, container,
updatedTextElement as ExcalidrawTextElementWithContainer, updatedTextElement as ExcalidrawTextElementWithContainer,
elementsMap,
); );
coordX = boundTextCoords.x; coordX = boundTextCoords.x;
coordY = boundTextCoords.y; coordY = boundTextCoords.y;
@ -200,6 +201,7 @@ export const textWysiwyg = ({
const { y } = computeBoundTextPosition( const { y } = computeBoundTextPosition(
container, container,
updatedTextElement as ExcalidrawTextElementWithContainer, updatedTextElement as ExcalidrawTextElementWithContainer,
elementsMap,
); );
coordY = y; coordY = y;
} }
@ -326,7 +328,7 @@ export const textWysiwyg = ({
} }
const container = getContainerElement( const container = getContainerElement(
element, element,
app.scene.getElementsMapIncludingDeleted(), app.scene.getNonDeletedElementsMap(),
); );
const font = getFontString({ const font = getFontString({
@ -513,7 +515,7 @@ export const textWysiwyg = ({
let text = editable.value; let text = editable.value;
const container = getContainerElement( const container = getContainerElement(
updateElement, updateElement,
app.scene.getElementsMapIncludingDeleted(), app.scene.getNonDeletedElementsMap(),
); );
if (container) { if (container) {
@ -541,7 +543,11 @@ export const textWysiwyg = ({
), ),
}); });
} }
redrawTextBoundingBox(updateElement, container); redrawTextBoundingBox(
updateElement,
container,
app.scene.getNonDeletedElementsMap(),
);
} }
onSubmit({ onSubmit({

View file

@ -1,4 +1,5 @@
import { import {
ElementsMap,
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
PointerType, PointerType,
@ -230,6 +231,8 @@ export const getTransformHandlesFromCoords = (
export const getTransformHandles = ( export const getTransformHandles = (
element: ExcalidrawElement, element: ExcalidrawElement,
zoom: Zoom, zoom: Zoom,
elementsMap: ElementsMap,
pointerType: PointerType = "mouse", pointerType: PointerType = "mouse",
): TransformHandles => { ): TransformHandles => {
// so that when locked element is selected (especially when you toggle lock // 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 + 8
: DEFAULT_TRANSFORM_HANDLE_SPACING; : DEFAULT_TRANSFORM_HANDLE_SPACING;
return getTransformHandlesFromCoords( return getTransformHandlesFromCoords(
getElementAbsoluteCoords(element, true), getElementAbsoluteCoords(element, elementsMap, true),
element.angle, element.angle,
zoom, zoom,
pointerType, pointerType,

View file

@ -65,10 +65,11 @@ export const bindElementsToFramesAfterDuplication = (
export function isElementIntersectingFrame( export function isElementIntersectingFrame(
element: ExcalidrawElement, element: ExcalidrawElement,
frame: ExcalidrawFrameLikeElement, 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) => const intersecting = frameLineSegments.some((frameLineSegment) =>
elementLineSegments.some((elementLineSegment) => elementLineSegments.some((elementLineSegment) =>
@ -82,9 +83,10 @@ export function isElementIntersectingFrame(
export const getElementsCompletelyInFrame = ( export const getElementsCompletelyInFrame = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => ) =>
omitGroupsContainingFrameLikes( omitGroupsContainingFrameLikes(
getElementsWithinSelection(elements, frame, false), getElementsWithinSelection(elements, frame, elementsMap, false),
).filter( ).filter(
(element) => (element) =>
(!isFrameLikeElement(element) && !element.frameId) || (!isFrameLikeElement(element) && !element.frameId) ||
@ -95,8 +97,9 @@ export const isElementContainingFrame = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
element: ExcalidrawElement, element: ExcalidrawElement,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
return getElementsWithinSelection(elements, element).some( return getElementsWithinSelection(elements, element, elementsMap).some(
(e) => e.id === frame.id, (e) => e.id === frame.id,
); );
}; };
@ -104,13 +107,22 @@ export const isElementContainingFrame = (
export const getElementsIntersectingFrame = ( export const getElementsIntersectingFrame = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => elements.filter((element) => isElementIntersectingFrame(element, frame)); ) => {
const elementsMap = arrayToMap(elements);
return elements.filter((element) =>
isElementIntersectingFrame(element, frame, elementsMap),
);
};
export const elementsAreInFrameBounds = ( export const elementsAreInFrameBounds = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame); const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(
frame,
elementsMap,
);
const [elementX1, elementY1, elementX2, elementY2] = const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements); getCommonBounds(elements);
@ -126,11 +138,12 @@ export const elementsAreInFrameBounds = (
export const elementOverlapsWithFrame = ( export const elementOverlapsWithFrame = (
element: ExcalidrawElement, element: ExcalidrawElement,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
return ( return (
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame, elementsMap) ||
isElementIntersectingFrame(element, frame) || isElementIntersectingFrame(element, frame, elementsMap) ||
isElementContainingFrame([frame], element, frame) isElementContainingFrame([frame], element, frame, elementsMap)
); );
}; };
@ -140,8 +153,9 @@ export const isCursorInFrame = (
y: number; y: number;
}, },
frame: NonDeleted<ExcalidrawFrameLikeElement>, frame: NonDeleted<ExcalidrawFrameLikeElement>,
elementsMap: ElementsMap,
) => { ) => {
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame); const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
return isPointWithinBounds( return isPointWithinBounds(
[fx1, fy1], [fx1, fy1],
@ -155,6 +169,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
groupIds: readonly string[], groupIds: readonly string[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => { ) => {
const elementsMap = arrayToMap(elements);
const elementsInGroup = groupIds.flatMap((groupId) => const elementsInGroup = groupIds.flatMap((groupId) =>
getElementsInGroup(elements, groupId), getElementsInGroup(elements, groupId),
); );
@ -165,8 +180,8 @@ export const groupsAreAtLeastIntersectingTheFrame = (
return !!elementsInGroup.find( return !!elementsInGroup.find(
(element) => (element) =>
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame, elementsMap) ||
isElementIntersectingFrame(element, frame), isElementIntersectingFrame(element, frame, elementsMap),
); );
}; };
@ -175,6 +190,7 @@ export const groupsAreCompletelyOutOfFrame = (
groupIds: readonly string[], groupIds: readonly string[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => { ) => {
const elementsMap = arrayToMap(elements);
const elementsInGroup = groupIds.flatMap((groupId) => const elementsInGroup = groupIds.flatMap((groupId) =>
getElementsInGroup(elements, groupId), getElementsInGroup(elements, groupId),
); );
@ -186,8 +202,8 @@ export const groupsAreCompletelyOutOfFrame = (
return ( return (
elementsInGroup.find( elementsInGroup.find(
(element) => (element) =>
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame, elementsMap) ||
isElementIntersectingFrame(element, frame), isElementIntersectingFrame(element, frame, elementsMap),
) === undefined ) === undefined
); );
}; };
@ -258,14 +274,15 @@ export const getElementsInResizingFrame = (
allElements: ExcalidrawElementsIncludingDeleted, allElements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const prevElementsInFrame = getFrameChildren(allElements, frame.id); const prevElementsInFrame = getFrameChildren(allElements, frame.id);
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame); const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
const elementsCompletelyInFrame = new Set([ const elementsCompletelyInFrame = new Set([
...getElementsCompletelyInFrame(allElements, frame), ...getElementsCompletelyInFrame(allElements, frame, elementsMap),
...prevElementsInFrame.filter((element) => ...prevElementsInFrame.filter((element) =>
isElementContainingFrame(allElements, element, frame), isElementContainingFrame(allElements, element, frame, elementsMap),
), ),
]); ]);
@ -283,7 +300,7 @@ export const getElementsInResizingFrame = (
); );
for (const element of elementsNotCompletelyInFrame) { for (const element of elementsNotCompletelyInFrame) {
if (!isElementIntersectingFrame(element, frame)) { if (!isElementIntersectingFrame(element, frame, elementsMap)) {
if (element.groupIds.length === 0) { if (element.groupIds.length === 0) {
nextElementsInFrame.delete(element); nextElementsInFrame.delete(element);
} }
@ -334,7 +351,7 @@ export const getElementsInResizingFrame = (
if (isSelected) { if (isSelected) {
const elementsInGroup = getElementsInGroup(allElements, id); const elementsInGroup = getElementsInGroup(allElements, id);
if (elementsAreInFrameBounds(elementsInGroup, frame)) { if (elementsAreInFrameBounds(elementsInGroup, frame, elementsMap)) {
for (const element of elementsInGroup) { for (const element of elementsInGroup) {
nextElementsInFrame.add(element); nextElementsInFrame.add(element);
} }
@ -348,12 +365,13 @@ export const getElementsInResizingFrame = (
}; };
export const getElementsInNewFrame = ( export const getElementsInNewFrame = (
allElements: ExcalidrawElementsIncludingDeleted, elements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
return omitGroupsContainingFrameLikes( return omitGroupsContainingFrameLikes(
allElements, elements,
getElementsCompletelyInFrame(allElements, frame), getElementsCompletelyInFrame(elements, frame, elementsMap),
); );
}; };
@ -388,7 +406,7 @@ export const filterElementsEligibleAsFrameChildren = (
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => { ) => {
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>(); const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
const elementsMap = arrayToMap(elements);
elements = omitGroupsContainingFrameLikes(elements); elements = omitGroupsContainingFrameLikes(elements);
for (const element of elements) { for (const element of elements) {
@ -415,14 +433,18 @@ export const filterElementsEligibleAsFrameChildren = (
if (!processedGroups.has(shallowestGroupId)) { if (!processedGroups.has(shallowestGroupId)) {
processedGroups.add(shallowestGroupId); processedGroups.add(shallowestGroupId);
const groupElements = getElementsInGroup(elements, 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) { for (const child of groupElements) {
eligibleElements.push(child); eligibleElements.push(child);
} }
} }
} }
} else { } else {
const overlaps = elementOverlapsWithFrame(element, frame); const overlaps = elementOverlapsWithFrame(element, frame, elementsMap);
if (overlaps) { if (overlaps) {
eligibleElements.push(element); eligibleElements.push(element);
} }
@ -682,12 +704,12 @@ export const getTargetFrame = (
// given an element, return if the element is in some frame // given an element, return if the element is in some frame
export const isElementInFrame = ( export const isElementInFrame = (
element: ExcalidrawElement, element: ExcalidrawElement,
allElements: ElementsMap, allElementsMap: ElementsMap,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
) => { ) => {
const frame = getTargetFrame(element, allElements, appState); const frame = getTargetFrame(element, allElementsMap, appState);
const _element = isTextElement(element) const _element = isTextElement(element)
? getContainerElement(element, allElements) || element ? getContainerElement(element, allElementsMap) || element
: element; : element;
if (frame) { if (frame) {
@ -703,16 +725,18 @@ export const isElementInFrame = (
} }
if (_element.groupIds.length === 0) { if (_element.groupIds.length === 0) {
return elementOverlapsWithFrame(_element, frame); return elementOverlapsWithFrame(_element, frame, allElementsMap);
} }
const allElementsInGroup = new Set( const allElementsInGroup = new Set(
_element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)), _element.groupIds.flatMap((gid) =>
getElementsInGroup(allElementsMap, gid),
),
); );
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) { if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
const selectedElements = new Set( const selectedElements = new Set(
getSelectedElements(allElements, appState), getSelectedElements(allElementsMap, appState),
); );
const editingGroupOverlapsFrame = appState.frameToHighlight !== null; const editingGroupOverlapsFrame = appState.frameToHighlight !== null;
@ -733,7 +757,7 @@ export const isElementInFrame = (
} }
for (const elementInGroup of allElementsInGroup) { for (const elementInGroup of allElementsInGroup) {
if (elementOverlapsWithFrame(elementInGroup, frame)) { if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) {
return true; return true;
} }
} }

View file

@ -7,6 +7,7 @@ import {
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
ElementsMap,
} from "../element/types"; } from "../element/types";
import { import {
isTextElement, isTextElement,
@ -137,6 +138,7 @@ export interface ExcalidrawElementWithCanvas {
const cappedElementCanvasSize = ( const cappedElementCanvasSize = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
zoom: Zoom, zoom: Zoom,
): { ): {
width: number; width: number;
@ -155,7 +157,7 @@ const cappedElementCanvasSize = (
const padding = getCanvasPadding(element); const padding = getCanvasPadding(element);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const elementWidth = const elementWidth =
isLinearElement(element) || isFreeDrawElement(element) isLinearElement(element) || isFreeDrawElement(element)
? distance(x1, x2) ? distance(x1, x2)
@ -200,7 +202,11 @@ const generateElementCanvas = (
const context = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
const padding = getCanvasPadding(element); const padding = getCanvasPadding(element);
const { width, height, scale } = cappedElementCanvasSize(element, zoom); const { width, height, scale } = cappedElementCanvasSize(
element,
elementsMap,
zoom,
);
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
@ -209,7 +215,7 @@ const generateElementCanvas = (
let canvasOffsetY = 0; let canvasOffsetY = 0;
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
const [x1, y1] = getElementAbsoluteCoords(element); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
canvasOffsetX = canvasOffsetX =
element.x > x1 element.x > x1
@ -468,7 +474,7 @@ const drawElementFromCanvas = (
const element = elementWithCanvas.element; const element = elementWithCanvas.element;
const padding = getCanvasPadding(element); const padding = getCanvasPadding(element);
const zoom = elementWithCanvas.scale; 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 // Free draw elements will otherwise "shuffle" as the min x and y change
if (isFreeDrawElement(element)) { if (isFreeDrawElement(element)) {
@ -513,8 +519,10 @@ const drawElementFromCanvas = (
elementWithCanvas.canvas.height, elementWithCanvas.canvas.height,
); );
const [, , , , boundTextCx, boundTextCy] = const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
getElementAbsoluteCoords(boundTextElement); boundTextElement,
allElementsMap,
);
tempCanvasContext.rotate(-element.angle); tempCanvasContext.rotate(-element.angle);
@ -694,7 +702,7 @@ export const renderElement = (
ShapeCache.generateElementShape(element, null); ShapeCache.generateElementShape(element, null);
if (renderConfig.isExporting) { 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 cx = (x1 + x2) / 2 + appState.scrollX;
const cy = (y1 + y2) / 2 + appState.scrollY; const cy = (y1 + y2) / 2 + appState.scrollY;
const shiftX = (x2 - x1) / 2 - (element.x - x1); const shiftX = (x2 - x1) / 2 - (element.x - x1);
@ -737,7 +745,7 @@ export const renderElement = (
// rely on existing shapes // rely on existing shapes
ShapeCache.generateElementShape(element, renderConfig); ShapeCache.generateElementShape(element, renderConfig);
if (renderConfig.isExporting) { 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 cx = (x1 + x2) / 2 + appState.scrollX;
const cy = (y1 + y2) / 2 + appState.scrollY; const cy = (y1 + y2) / 2 + appState.scrollY;
let shiftX = (x2 - x1) / 2 - (element.x - x1); let shiftX = (x2 - x1) / 2 - (element.x - x1);
@ -749,6 +757,7 @@ export const renderElement = (
LinearElementEditor.getBoundTextElementPosition( LinearElementEditor.getBoundTextElementPosition(
container, container,
element as ExcalidrawTextElementWithContainer, element as ExcalidrawTextElementWithContainer,
elementsMap,
); );
shiftX = (x2 - x1) / 2 - (boundTextCoords.x - x1); shiftX = (x2 - x1) / 2 - (boundTextCoords.x - x1);
shiftY = (y2 - y1) / 2 - (boundTextCoords.y - y1); shiftY = (y2 - y1) / 2 - (boundTextCoords.y - y1);
@ -804,8 +813,10 @@ export const renderElement = (
tempCanvasContext.rotate(-element.angle); tempCanvasContext.rotate(-element.angle);
// Shift the canvas to center of bound text // Shift the canvas to center of bound text
const [, , , , boundTextCx, boundTextCy] = const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
getElementAbsoluteCoords(boundTextElement); boundTextElement,
elementsMap,
);
const boundTextShiftX = (x1 + x2) / 2 - boundTextCx; const boundTextShiftX = (x1 + x2) / 2 - boundTextCx;
const boundTextShiftY = (y1 + y2) / 2 - boundTextCy; const boundTextShiftY = (y1 + y2) / 2 - boundTextCy;
tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY); tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY);
@ -939,17 +950,18 @@ export const renderElementToSvg = (
renderConfig: SVGRenderConfig, renderConfig: SVGRenderConfig,
) => { ) => {
const offset = { x: offsetX, y: offsetY }; 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 cx = (x2 - x1) / 2 - (element.x - x1);
let cy = (y2 - y1) / 2 - (element.y - y1); let cy = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) { if (isTextElement(element)) {
const container = getContainerElement(element, elementsMap); const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) { if (isArrowElement(container)) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container); const [x1, y1, x2, y2] = getElementAbsoluteCoords(container, elementsMap);
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
container, container,
element as ExcalidrawTextElementWithContainer, element as ExcalidrawTextElementWithContainer,
elementsMap,
); );
cx = (x2 - x1) / 2 - (boundTextCoords.x - x1); cx = (x2 - x1) / 2 - (boundTextCoords.x - x1);
cy = (y2 - y1) / 2 - (boundTextCoords.y - y1); cy = (y2 - y1) / 2 - (boundTextCoords.y - y1);
@ -1151,6 +1163,7 @@ export const renderElementToSvg = (
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
element, element,
boundText, boundText,
elementsMap,
); );
const maskX = offsetX + boundTextCoords.x - element.x; const maskX = offsetX + boundTextCoords.x - element.x;

View file

@ -17,6 +17,7 @@ import {
GroupId, GroupId,
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
ElementsMap,
} from "../element/types"; } from "../element/types";
import { import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
@ -256,7 +257,10 @@ const renderLinearPointHandles = (
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
context.lineWidth = 1 / appState.zoom.value; context.lineWidth = 1 / appState.zoom.value;
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
const { POINT_HANDLE_SIZE } = LinearElementEditor; const { POINT_HANDLE_SIZE } = LinearElementEditor;
const radius = appState.editingLinearElement const radius = appState.editingLinearElement
@ -340,6 +344,7 @@ const highlightPoint = (
const renderLinearElementPointHighlight = ( const renderLinearElementPointHighlight = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
elementsMap: ElementsMap,
) => { ) => {
const { elementId, hoverPointIndex } = appState.selectedLinearElement!; const { elementId, hoverPointIndex } = appState.selectedLinearElement!;
if ( if (
@ -356,6 +361,7 @@ const renderLinearElementPointHighlight = (
const point = LinearElementEditor.getPointAtIndexGlobalCoordinates( const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
hoverPointIndex, hoverPointIndex,
elementsMap,
); );
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
@ -510,12 +516,22 @@ const _renderInteractiveScene = ({
appState.suggestedBindings appState.suggestedBindings
.filter((binding) => binding != null) .filter((binding) => binding != null)
.forEach((suggestedBinding) => { .forEach((suggestedBinding) => {
renderBindingHighlight(context, appState, suggestedBinding!); renderBindingHighlight(
context,
appState,
suggestedBinding!,
elementsMap,
);
}); });
} }
if (appState.frameToHighlight) { if (appState.frameToHighlight) {
renderFrameHighlight(context, appState, appState.frameToHighlight); renderFrameHighlight(
context,
appState,
appState.frameToHighlight,
elementsMap,
);
} }
if (appState.elementsToHighlight) { if (appState.elementsToHighlight) {
@ -545,7 +561,7 @@ const _renderInteractiveScene = ({
appState.selectedLinearElement && appState.selectedLinearElement &&
appState.selectedLinearElement.hoverPointIndex >= 0 appState.selectedLinearElement.hoverPointIndex >= 0
) { ) {
renderLinearElementPointHighlight(context, appState); renderLinearElementPointHighlight(context, appState, elementsMap);
} }
// Paint selected elements // Paint selected elements
if (!appState.multiElement && !appState.editingLinearElement) { if (!appState.multiElement && !appState.editingLinearElement) {
@ -608,7 +624,7 @@ const _renderInteractiveScene = ({
if (selectionColors.length) { if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] = const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
getElementAbsoluteCoords(element, true); getElementAbsoluteCoords(element, elementsMap, true);
selections.push({ selections.push({
angle: element.angle, angle: element.angle,
elementX1, elementX1,
@ -666,7 +682,8 @@ const _renderInteractiveScene = ({
const transformHandles = getTransformHandles( const transformHandles = getTransformHandles(
selectedElements[0], selectedElements[0],
appState.zoom, 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) { if (!appState.viewModeEnabled && showBoundingBox) {
renderTransformHandles( renderTransformHandles(
@ -953,7 +970,11 @@ const _renderStaticScene = ({
element.groupIds.length > 0 && element.groupIds.length > 0 &&
appState.frameToHighlight && appState.frameToHighlight &&
appState.selectedElementIds[element.id] && appState.selectedElementIds[element.id] &&
(elementOverlapsWithFrame(element, appState.frameToHighlight) || (elementOverlapsWithFrame(
element,
appState.frameToHighlight,
elementsMap,
) ||
element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId))) element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId)))
) { ) {
element.groupIds.forEach((groupId) => element.groupIds.forEach((groupId) =>
@ -1004,7 +1025,7 @@ const _renderStaticScene = ({
); );
} }
if (!isExporting) { if (!isExporting) {
renderLinkIcon(element, context, appState); renderLinkIcon(element, context, appState, elementsMap);
} }
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
@ -1048,7 +1069,7 @@ const _renderStaticScene = ({
); );
} }
if (!isExporting) { if (!isExporting) {
renderLinkIcon(element, context, appState); renderLinkIcon(element, context, appState, elementsMap);
} }
}; };
// - when exporting the whole canvas, we DO NOT apply clipping // - when exporting the whole canvas, we DO NOT apply clipping
@ -1247,6 +1268,7 @@ const renderBindingHighlight = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
suggestedBinding: SuggestedBinding, suggestedBinding: SuggestedBinding,
elementsMap: ElementsMap,
) => { ) => {
const renderHighlight = Array.isArray(suggestedBinding) const renderHighlight = Array.isArray(suggestedBinding)
? renderBindingHighlightForSuggestedPointBinding ? renderBindingHighlightForSuggestedPointBinding
@ -1254,7 +1276,7 @@ const renderBindingHighlight = (
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
renderHighlight(context, suggestedBinding as any); renderHighlight(context, suggestedBinding as any, elementsMap);
context.restore(); context.restore();
}; };
@ -1262,8 +1284,9 @@ const renderBindingHighlight = (
const renderBindingHighlightForBindableElement = ( const renderBindingHighlightForBindableElement = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const width = x2 - x1; const width = x2 - x1;
const height = y2 - y1; const height = y2 - y1;
const threshold = maxBindingGap(element, width, height); const threshold = maxBindingGap(element, width, height);
@ -1323,8 +1346,9 @@ const renderFrameHighlight = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
frame: NonDeleted<ExcalidrawFrameLikeElement>, frame: NonDeleted<ExcalidrawFrameLikeElement>,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame); const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
const width = x2 - x1; const width = x2 - x1;
const height = y2 - y1; const height = y2 - y1;
@ -1398,6 +1422,7 @@ const renderElementsBoxHighlight = (
const renderBindingHighlightForSuggestedPointBinding = ( const renderBindingHighlightForSuggestedPointBinding = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
suggestedBinding: SuggestedPointBinding, suggestedBinding: SuggestedPointBinding,
elementsMap: ElementsMap,
) => { ) => {
const [element, startOrEnd, bindableElement] = suggestedBinding; const [element, startOrEnd, bindableElement] = suggestedBinding;
@ -1416,6 +1441,7 @@ const renderBindingHighlightForSuggestedPointBinding = (
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
index, index,
elementsMap,
); );
fillCircle(context, x, y, threshold); fillCircle(context, x, y, threshold);
}); });
@ -1426,9 +1452,10 @@ const renderLinkIcon = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
elementsMap: ElementsMap,
) => { ) => {
if (element.link && !appState.selectedElementIds[element.id]) { 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( const [x, y, width, height] = getLinkHandleFromCoords(
[x1, y1, x2, y2], [x1, y1, x2, y2],
element.angle, element.angle,

View file

@ -60,10 +60,8 @@ export class Fonts {
return newElementWith(element, { return newElementWith(element, {
...refreshTextDimensions( ...refreshTextDimensions(
element, element,
getContainerElement( getContainerElement(element, this.scene.getNonDeletedElementsMap()),
element, this.scene.getNonDeletedElementsMap(),
this.scene.getElementsMapIncludingDeleted(),
),
), ),
}); });
} }

View file

@ -392,8 +392,9 @@ export const exportToSvg = async (
const frameElements = getFrameLikeElements(elements); const frameElements = getFrameLikeElements(elements);
let exportingFrameClipPath = ""; let exportingFrameClipPath = "";
const elementsMap = arrayToMap(elements);
for (const frame of frameElements) { 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 cx = (x2 - x1) / 2 - (frame.x - x1);
const cy = (y2 - y1) / 2 - (frame.y - y1); const cy = (y2 - y1) / 2 - (frame.y - y1);

View file

@ -1,4 +1,5 @@
import { import {
ElementsMap,
ElementsMapOrArray, ElementsMapOrArray,
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
@ -44,10 +45,11 @@ export const excludeElementsInFramesFromSelection = <
export const getElementsWithinSelection = ( export const getElementsWithinSelection = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selection: NonDeletedExcalidrawElement, selection: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
excludeElementsInFrames: boolean = true, excludeElementsInFrames: boolean = true,
) => { ) => {
const [selectionX1, selectionY1, selectionX2, selectionY2] = const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(selection); getElementAbsoluteCoords(selection, elementsMap);
let elementsInSelection = elements.filter((element) => { let elementsInSelection = elements.filter((element) => {
let [elementX1, elementY1, elementX2, elementY2] = let [elementX1, elementY1, elementX2, elementY2] =
@ -82,7 +84,7 @@ export const getElementsWithinSelection = (
const containingFrame = getContainingFrame(element); const containingFrame = getContainingFrame(element);
if (containingFrame) { if (containingFrame) {
return elementOverlapsWithFrame(element, containingFrame); return elementOverlapsWithFrame(element, containingFrame, elementsMap);
} }
return true; return true;

View file

@ -8,15 +8,18 @@ import {
import { MaybeTransformHandleType } from "./element/transformHandles"; import { MaybeTransformHandleType } from "./element/transformHandles";
import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks"; import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks";
import { import {
ElementsMap,
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./element/types"; } from "./element/types";
import { getMaximumGroups } from "./groups"; import { getMaximumGroups } from "./groups";
import { KEYS } from "./keys"; import { KEYS } from "./keys";
import { rangeIntersection, rangesOverlap, rotatePoint } from "./math"; import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
import { getVisibleAndNonSelectedElements } from "./scene/selection"; import {
getSelectedElements,
getVisibleAndNonSelectedElements,
} from "./scene/selection";
import { AppState, KeyboardModifiersObject, Point } from "./types"; import { AppState, KeyboardModifiersObject, Point } from "./types";
import { arrayToMap } from "./utils";
const SNAP_DISTANCE = 8; const SNAP_DISTANCE = 8;
@ -167,6 +170,7 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => {
export const getElementsCorners = ( export const getElementsCorners = (
elements: ExcalidrawElement[], elements: ExcalidrawElement[],
elementsMap: ElementsMap,
{ {
omitCenter, omitCenter,
boundingBoxCorners, boundingBoxCorners,
@ -185,7 +189,10 @@ export const getElementsCorners = (
if (elements.length === 1) { if (elements.length === 1) {
const element = elements[0]; 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) { if (dragOffset) {
x1 += dragOffset.x; x1 += dragOffset.x;
@ -280,6 +287,7 @@ export const getVisibleGaps = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const referenceElements: ExcalidrawElement[] = getReferenceElements( const referenceElements: ExcalidrawElement[] = getReferenceElements(
elements, elements,
@ -287,10 +295,7 @@ export const getVisibleGaps = (
appState, appState,
); );
const referenceBounds = getMaximumGroups( const referenceBounds = getMaximumGroups(referenceElements, elementsMap)
referenceElements,
arrayToMap(elements),
)
.filter( .filter(
(elementsGroup) => (elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
@ -569,19 +574,19 @@ export const getReferenceSnapPoints = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const referenceElements = getReferenceElements( const referenceElements = getReferenceElements(
elements, elements,
selectedElements, selectedElements,
appState, appState,
); );
return getMaximumGroups(referenceElements, elementsMap)
return getMaximumGroups(referenceElements, arrayToMap(elements))
.filter( .filter(
(elementsGroup) => (elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
) )
.flatMap((elementGroup) => getElementsCorners(elementGroup)); .flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
}; };
const getPointSnaps = ( const getPointSnaps = (
@ -641,11 +646,13 @@ const getPointSnaps = (
}; };
export const snapDraggedElements = ( export const snapDraggedElements = (
selectedElements: ExcalidrawElement[], elements: ExcalidrawElement[],
dragOffset: Vector2D, dragOffset: Vector2D,
appState: AppState, appState: AppState,
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
elementsMap: ElementsMap,
) => { ) => {
const selectedElements = getSelectedElements(elements, appState);
if ( if (
!isSnappingEnabled({ appState, event, selectedElements }) || !isSnappingEnabled({ appState, event, selectedElements }) ||
selectedElements.length === 0 selectedElements.length === 0
@ -658,7 +665,6 @@ export const snapDraggedElements = (
snapLines: [], snapLines: [],
}; };
} }
dragOffset.x = round(dragOffset.x); dragOffset.x = round(dragOffset.x);
dragOffset.y = round(dragOffset.y); dragOffset.y = round(dragOffset.y);
const nearestSnapsX: Snaps = []; const nearestSnapsX: Snaps = [];
@ -669,7 +675,7 @@ export const snapDraggedElements = (
y: snapDistance, y: snapDistance,
}; };
const selectionPoints = getElementsCorners(selectedElements, { const selectionPoints = getElementsCorners(selectedElements, elementsMap, {
dragOffset, dragOffset,
}); });
@ -719,7 +725,7 @@ export const snapDraggedElements = (
getPointSnaps( getPointSnaps(
selectedElements, selectedElements,
getElementsCorners(selectedElements, { getElementsCorners(selectedElements, elementsMap, {
dragOffset: newDragOffset, dragOffset: newDragOffset,
}), }),
appState, appState,
@ -1204,6 +1210,7 @@ export const snapNewElement = (
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
origin: Vector2D, origin: Vector2D,
dragOffset: Vector2D, dragOffset: Vector2D,
elementsMap: ElementsMap,
) => { ) => {
if ( if (
!isSnappingEnabled({ event, selectedElements: [draggingElement], appState }) !isSnappingEnabled({ event, selectedElements: [draggingElement], appState })
@ -1248,7 +1255,7 @@ export const snapNewElement = (
nearestSnapsX.length = 0; nearestSnapsX.length = 0;
nearestSnapsY.length = 0; nearestSnapsY.length = 0;
const corners = getElementsCorners([draggingElement], { const corners = getElementsCorners([draggingElement], elementsMap, {
boundingBoxCorners: true, boundingBoxCorners: true,
omitCenter: true, omitCenter: true,
}); });
@ -1276,6 +1283,7 @@ export const getSnapLinesAtPointer = (
appState: AppState, appState: AppState,
pointer: Vector2D, pointer: Vector2D,
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
elementsMap: ElementsMap,
) => { ) => {
if (!isSnappingEnabled({ event, selectedElements: [], appState })) { if (!isSnappingEnabled({ event, selectedElements: [], appState })) {
return { return {
@ -1301,7 +1309,7 @@ export const getSnapLinesAtPointer = (
const verticalSnapLines: PointerSnapLine[] = []; const verticalSnapLines: PointerSnapLine[] = [];
for (const referenceElement of referenceElements) { for (const referenceElement of referenceElements) {
const corners = getElementsCorners([referenceElement]); const corners = getElementsCorners([referenceElement], elementsMap);
for (const corner of corners) { for (const corner of corners) {
const offsetX = corner[0] - pointer.x; const offsetX = corner[0] - pointer.x;

View file

@ -5,6 +5,7 @@ import { getTransformHandles } from "../element/transformHandles";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import { arrayToMap } from "../utils";
const { h } = window; const { h } = window;
@ -91,8 +92,12 @@ describe("element binding", () => {
expect(arrow.startBinding?.elementId).toBe(rectLeft.id); expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
expect(arrow.endBinding?.elementId).toBe(rectRight.id); expect(arrow.endBinding?.elementId).toBe(rectRight.id);
const rotation = getTransformHandles(arrow, h.state.zoom, "mouse") const rotation = getTransformHandles(
.rotation!; arrow,
h.state.zoom,
arrayToMap(h.elements),
"mouse",
).rotation!;
const rotationHandleX = rotation[0] + rotation[2] / 2; const rotationHandleX = rotation[0] + rotation[2] / 2;
const rotationHandleY = rotation[1] + rotation[3] / 2; const rotationHandleY = rotation[1] + rotation[3] / 2;
mouse.down(rotationHandleX, rotationHandleY); mouse.down(rotationHandleX, rotationHandleY);

View file

@ -27,7 +27,7 @@ import * as blob from "../data/blob";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getBoundTextElementPosition } from "../element/textElement"; import { getBoundTextElementPosition } from "../element/textElement";
import { createPasteEvent } from "../clipboard"; import { createPasteEvent } from "../clipboard";
import { cloneJSON } from "../utils"; import { arrayToMap, cloneJSON } from "../utils";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@ -194,9 +194,10 @@ const checkElementsBoundingBox = async (
element2: ExcalidrawElement, element2: ExcalidrawElement,
toleranceInPx: number = 0, 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(() => { await waitFor(() => {
// Check if width and height did not change // Check if width and height did not change
@ -853,7 +854,11 @@ describe("mutliple elements", () => {
h.app.actionManager.executeAction(actionFlipVertical); h.app.actionManager.executeAction(actionFlipVertical);
const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer; 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; const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
expect(arrow.x).toBeCloseTo(180); expect(arrow.x).toBeCloseTo(180);

View file

@ -32,6 +32,7 @@ import {
import { getCommonBounds, getElementPointsCoords } from "../../element/bounds"; import { getCommonBounds, getElementPointsCoords } from "../../element/bounds";
import { rotatePoint } from "../../math"; import { rotatePoint } from "../../math";
import { getTextEditor } from "../queries/dom"; import { getTextEditor } from "../queries/dom";
import { arrayToMap } from "../../utils";
const { h } = window; const { h } = window;
@ -286,9 +287,12 @@ const transform = (
let handleCoords: TransformHandle | undefined; let handleCoords: TransformHandle | undefined;
if (elements.length === 1) { if (elements.length === 1) {
handleCoords = getTransformHandles(elements[0], h.state.zoom, "mouse")[ handleCoords = getTransformHandles(
handle elements[0],
]; h.state.zoom,
arrayToMap(h.elements),
"mouse",
)[handle];
} else { } else {
const [x1, y1, x2, y2] = getCommonBounds(elements); const [x1, y1, x2, y2] = getCommonBounds(elements);
const isFrameSelected = elements.some(isFrameLikeElement); const isFrameSelected = elements.some(isFrameLikeElement);

View file

@ -343,6 +343,8 @@ describe("Test Linear Elements", () => {
}); });
it("should update all the midpoints when element position changed", async () => { it("should update all the midpoints when element position changed", async () => {
const elementsMap = arrayToMap(h.elements);
createThreePointerLinearElement("line", { createThreePointerLinearElement("line", {
type: ROUNDNESS.PROPORTIONAL_RADIUS, type: ROUNDNESS.PROPORTIONAL_RADIUS,
}); });
@ -351,7 +353,10 @@ describe("Test Linear Elements", () => {
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
enterLineEditingMode(line); enterLineEditingMode(line);
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
expect([line.x, line.y]).toEqual(points[0]); expect([line.x, line.y]).toEqual(points[0]);
const midPoints = LinearElementEditor.getEditorMidPoints( 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 () => { 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( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@ -482,7 +491,10 @@ describe("Test Linear Elements", () => {
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); 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([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] - delta, points[0][0] - delta,
points[0][1] - 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 () => { 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( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@ -516,7 +532,10 @@ describe("Test Linear Elements", () => {
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); 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([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] + delta, points[0][0] + delta,
points[0][1] + 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 () => { it("should remove the midpoint when one of the points in the segment is deleted", async () => {
const line = h.elements[0] as ExcalidrawLinearElement; const line = h.elements[0] as ExcalidrawLinearElement;
enterLineEditingMode(line); enterLineEditingMode(line);
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
arrayToMap(h.elements),
);
// dragging line from last segment midpoint // dragging line from last segment midpoint
drag(lastSegmentMidpoint, [ drag(lastSegmentMidpoint, [
@ -637,7 +659,11 @@ describe("Test Linear Elements", () => {
}); });
it("should update all the midpoints when its point is dragged", async () => { 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( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@ -649,7 +675,10 @@ describe("Test Linear Elements", () => {
// Drag from first point // Drag from first point
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]); 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([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] - delta, points[0][0] - delta,
points[0][1] - 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 () => { 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( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@ -695,7 +728,10 @@ describe("Test Linear Elements", () => {
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); 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([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] + delta, points[0][0] + delta,
points[0][1] + 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 () => { it("should update all the midpoints when a point is deleted", async () => {
const elementsMap = arrayToMap(h.elements);
drag(lastSegmentMidpoint, [ drag(lastSegmentMidpoint, [
lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta, lastSegmentMidpoint[1] + delta,
@ -723,7 +761,10 @@ describe("Test Linear Elements", () => {
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
h.state, h.state,
); );
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
// delete 3rd point // delete 3rd point
deletePoint(points[2]); deletePoint(points[2]);
@ -837,6 +878,7 @@ describe("Test Linear Elements", () => {
const position = LinearElementEditor.getBoundTextElementPosition( const position = LinearElementEditor.getBoundTextElementPosition(
container, container,
textElement, textElement,
arrayToMap(h.elements),
); );
expect(position).toMatchInlineSnapshot(` expect(position).toMatchInlineSnapshot(`
{ {
@ -859,6 +901,7 @@ describe("Test Linear Elements", () => {
const position = LinearElementEditor.getBoundTextElementPosition( const position = LinearElementEditor.getBoundTextElementPosition(
container, container,
textElement, textElement,
arrayToMap(h.elements),
); );
expect(position).toMatchInlineSnapshot(` expect(position).toMatchInlineSnapshot(`
{ {
@ -893,6 +936,7 @@ describe("Test Linear Elements", () => {
const position = LinearElementEditor.getBoundTextElementPosition( const position = LinearElementEditor.getBoundTextElementPosition(
container, container,
textElement, textElement,
arrayToMap(h.elements),
); );
expect(position).toMatchInlineSnapshot(` expect(position).toMatchInlineSnapshot(`
{ {
@ -1012,8 +1056,13 @@ describe("Test Linear Elements", () => {
); );
expect(container.width).toBe(70); expect(container.width).toBe(70);
expect(container.height).toBe(50); expect(container.height).toBe(50);
expect(getBoundTextElementPosition(container, textElement)) expect(
.toMatchInlineSnapshot(` getBoundTextElementPosition(
container,
textElement,
arrayToMap(h.elements),
),
).toMatchInlineSnapshot(`
{ {
"x": 75, "x": 75,
"y": 60, "y": 60,
@ -1051,8 +1100,13 @@ describe("Test Linear Elements", () => {
} }
`); `);
expect(getBoundTextElementPosition(container, textElement)) expect(
.toMatchInlineSnapshot(` getBoundTextElementPosition(
container,
textElement,
arrayToMap(h.elements),
),
).toMatchInlineSnapshot(`
{ {
"x": 271.11716195150507, "x": 271.11716195150507,
"y": 45, "y": 45,
@ -1090,7 +1144,8 @@ describe("Test Linear Elements", () => {
arrow, arrow,
); );
expect(container.width).toBe(40); expect(container.width).toBe(40);
expect(getBoundTextElementPosition(container, textElement)) const elementsMap = arrayToMap(h.elements);
expect(getBoundTextElementPosition(container, textElement, elementsMap))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
{ {
"x": 25, "x": 25,
@ -1102,7 +1157,10 @@ describe("Test Linear Elements", () => {
collaboration made collaboration made
easy" easy"
`); `);
const points = LinearElementEditor.getPointsGlobalCoordinates(container); const points = LinearElementEditor.getPointsGlobalCoordinates(
container,
elementsMap,
);
// Drag from last point // Drag from last point
drag(points[1], [points[1][0] + 300, points[1][1]]); 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(` .toMatchInlineSnapshot(`
{ {
"x": 75, "x": 75,

View file

@ -13,6 +13,7 @@ import {
import { UI, Pointer, Keyboard } from "./helpers/ui"; import { UI, Pointer, Keyboard } from "./helpers/ui";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { vi } from "vitest"; import { vi } from "vitest";
import { arrayToMap } from "../utils";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -75,12 +76,13 @@ describe("move element", () => {
const rectA = UI.createElement("rectangle", { size: 100 }); const rectA = UI.createElement("rectangle", { size: 100 });
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const line = UI.createElement("line", { x: 110, y: 50, size: 80 }); const line = UI.createElement("line", { x: 110, y: 50, size: 80 });
const elementsMap = arrayToMap(h.elements);
// bind line to two rectangles // bind line to two rectangles
bindOrUnbindLinearElement( bindOrUnbindLinearElement(
line.get() as NonDeleted<ExcalidrawLinearElement>, line.get() as NonDeleted<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement, rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement, rectB.get() as ExcalidrawRectangleElement,
elementsMap,
); );
// select the second rectangles // select the second rectangles

View file

@ -13,6 +13,7 @@ import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isLinearElement } from "../element/typeChecks"; import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils";
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -301,10 +302,12 @@ describe("arrow element", () => {
], ],
}); });
const label = await UI.editText(arrow, "Hello"); const label = await UI.editText(arrow, "Hello");
const elementsMap = arrayToMap(h.elements);
UI.resize(arrow, "se", [50, 30]); UI.resize(arrow, "se", [50, 30]);
let labelPos = LinearElementEditor.getBoundTextElementPosition( let labelPos = LinearElementEditor.getBoundTextElementPosition(
arrow, arrow,
label, label,
elementsMap,
); );
expect(labelPos.x + label.width / 2).toBeCloseTo( expect(labelPos.x + label.width / 2).toBeCloseTo(
@ -317,7 +320,11 @@ describe("arrow element", () => {
expect(label.fontSize).toEqual(20); expect(label.fontSize).toEqual(20);
UI.resize(arrow, "w", [20, 0]); UI.resize(arrow, "w", [20, 0]);
labelPos = LinearElementEditor.getBoundTextElementPosition(arrow, label); labelPos = LinearElementEditor.getBoundTextElementPosition(
arrow,
label,
elementsMap,
);
expect(labelPos.x + label.width / 2).toBeCloseTo( expect(labelPos.x + label.width / 2).toBeCloseTo(
arrow.x + arrow.points[2][0], arrow.x + arrow.points[2][0],
@ -743,15 +750,17 @@ describe("multiple selection", () => {
const selectionTop = 20 - topArrowLabel.height / 2; const selectionTop = 20 - topArrowLabel.height / 2;
const move = [80, 0] as [number, number]; const move = [80, 0] as [number, number];
const scale = move[0] / selectionWidth + 1; const scale = move[0] / selectionWidth + 1;
const elementsMap = arrayToMap(h.elements);
UI.resize([topArrow.get(), bottomArrow.get()], "se", move); UI.resize([topArrow.get(), bottomArrow.get()], "se", move);
const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
topArrow, topArrow,
topArrowLabel, topArrowLabel,
elementsMap,
); );
const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
bottomArrow, bottomArrow,
bottomArrowLabel, bottomArrowLabel,
elementsMap,
); );
expect(topArrow.x).toBeCloseTo(0); expect(topArrow.x).toBeCloseTo(0);
@ -944,12 +953,13 @@ describe("multiple selection", () => {
const scaleX = move[0] / selectionWidth + 1; const scaleX = move[0] / selectionWidth + 1;
const scaleY = -scaleX; const scaleY = -scaleX;
const lineOrigBounds = getBoundsFromPoints(line); const lineOrigBounds = getBoundsFromPoints(line);
const elementsMap = arrayToMap(h.elements);
UI.resize([line, image, rectangle, boundArrow], "se", move); UI.resize([line, image, rectangle, boundArrow], "se", move);
const lineNewBounds = getBoundsFromPoints(line); const lineNewBounds = getBoundsFromPoints(line);
const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition( const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
boundArrow, boundArrow,
arrowLabel, arrowLabel,
elementsMap,
); );
expect(line.x).toBeCloseTo(60 * scaleX); expect(line.x).toBeCloseTo(60 * scaleX);