feat: fractional indexing (#7359)

* Introducing fractional indices as part of `element.index`

* Ensuring invalid fractional indices are always synchronized with the array order

* Simplifying reconciliation based on the fractional indices

* Moving reconciliation inside the `@excalidraw/excalidraw` package

---------

Co-authored-by: Marcel Mraz <marcel@excalidraw.com>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Ryan Di 2024-04-04 20:51:11 +08:00 committed by GitHub
parent bbdcd30a73
commit 32df5502ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 3640 additions and 2047 deletions

View file

@ -182,6 +182,7 @@ import {
IframeData,
ExcalidrawIframeElement,
ExcalidrawEmbeddableElement,
Ordered,
} from "../element/types";
import { getCenter, getDistance } from "../gesture";
import {
@ -276,6 +277,7 @@ import {
muteFSAbortError,
isTestEnv,
easeOut,
arrayToMap,
updateStable,
addEventListener,
normalizeEOL,
@ -407,7 +409,6 @@ import { ElementCanvasButton } from "./MagicButton";
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
import { EditorLocalStorage } from "../data/EditorLocalStorage";
import FollowMode from "./FollowMode/FollowMode";
import { AnimationFrameHandler } from "../animation-frame-handler";
import { AnimatedTrail } from "../animated-trail";
import { LaserTrails } from "../laser-trails";
@ -422,6 +423,7 @@ import {
} from "../element/collision";
import { textWysiwyg } from "../element/textWysiwyg";
import { isOverScrollBars } from "../scene/scrollbars";
import { syncInvalidIndices, syncMovedIndices } from "../fractionalIndex";
import {
isPointHittingLink,
isPointHittingLinkIcon,
@ -948,7 +950,7 @@ class App extends React.Component<AppProps, AppState> {
const embeddableElements = this.scene
.getNonDeletedElements()
.filter(
(el): el is NonDeleted<ExcalidrawIframeLikeElement> =>
(el): el is Ordered<NonDeleted<ExcalidrawIframeLikeElement>> =>
(isEmbeddableElement(el) &&
this.embedsValidationStatus.get(el.id) === true) ||
isIframeElement(el),
@ -2056,7 +2058,7 @@ class App extends React.Component<AppProps, AppState> {
locked: false,
});
this.scene.addNewElement(frame);
this.scene.insertElement(frame);
for (const child of selectedElements) {
mutateElement(child, { frameId: frame.id });
@ -3115,10 +3117,10 @@ class App extends React.Component<AppProps, AppState> {
},
);
const allElements = [
...this.scene.getElementsIncludingDeleted(),
...newElements,
];
const prevElements = this.scene.getElementsIncludingDeleted();
const nextElements = [...prevElements, ...newElements];
syncMovedIndices(nextElements, arrayToMap(newElements));
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
@ -3127,10 +3129,10 @@ class App extends React.Component<AppProps, AppState> {
newElements,
topLayerFrame,
);
addElementsToFrame(allElements, eligibleElements, topLayerFrame);
addElementsToFrame(nextElements, eligibleElements, topLayerFrame);
}
this.scene.replaceAllElements(allElements);
this.scene.replaceAllElements(nextElements);
newElements.forEach((newElement) => {
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
@ -3361,19 +3363,7 @@ class App extends React.Component<AppProps, AppState> {
return;
}
const frameId = textElements[0].frameId;
if (frameId) {
this.scene.insertElementsAtIndex(
textElements,
this.scene.getElementIndex(frameId),
);
} else {
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted(),
...textElements,
]);
}
this.scene.insertElements(textElements);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
@ -4489,7 +4479,7 @@ class App extends React.Component<AppProps, AppState> {
includeBoundTextElement: boolean = false,
includeLockedElements: boolean = false,
): NonDeleted<ExcalidrawElement>[] {
const iframeLikes: ExcalidrawIframeElement[] = [];
const iframeLikes: Ordered<ExcalidrawIframeElement>[] = [];
const elementsMap = this.scene.getNonDeletedElementsMap();
@ -4758,7 +4748,7 @@ class App extends React.Component<AppProps, AppState> {
const containerIndex = this.scene.getElementIndex(container.id);
this.scene.insertElementAtIndex(element, containerIndex + 1);
} else {
this.scene.addNewElement(element);
this.scene.insertElement(element);
}
}
@ -6639,7 +6629,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin,
this,
);
this.scene.addNewElement(element);
this.scene.insertElement(element);
this.setState({
draggingElement: element,
editingElement: element,
@ -6684,10 +6674,7 @@ class App extends React.Component<AppProps, AppState> {
height,
});
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted(),
element,
]);
this.scene.insertElement(element);
return element;
};
@ -6741,10 +6728,7 @@ class App extends React.Component<AppProps, AppState> {
link,
});
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted(),
element,
]);
this.scene.insertElement(element);
return element;
};
@ -6908,7 +6892,7 @@ class App extends React.Component<AppProps, AppState> {
this,
);
this.scene.addNewElement(element);
this.scene.insertElement(element);
this.setState({
draggingElement: element,
editingElement: element,
@ -6987,7 +6971,7 @@ class App extends React.Component<AppProps, AppState> {
draggingElement: element,
});
} else {
this.scene.addNewElement(element);
this.scene.insertElement(element);
this.setState({
multiElement: null,
draggingElement: element,
@ -7021,10 +7005,7 @@ class App extends React.Component<AppProps, AppState> {
? newMagicFrameElement(constructorOpts)
: newFrameElement(constructorOpts);
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted(),
frame,
]);
this.scene.insertElement(frame);
this.setState({
multiElement: null,
@ -7437,7 +7418,11 @@ class App extends React.Component<AppProps, AppState> {
nextElements.push(element);
}
}
const nextSceneElements = [...nextElements, ...elementsToAppend];
syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend));
bindTextToShapeAfterDuplication(
nextElements,
elementsToAppend,
@ -7454,6 +7439,7 @@ class App extends React.Component<AppProps, AppState> {
elementsToAppend,
oldIdToDuplicatedId,
);
this.scene.replaceAllElements(nextSceneElements);
this.maybeCacheVisibleGaps(event, selectedElements, true);
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
@ -8628,7 +8614,7 @@ class App extends React.Component<AppProps, AppState> {
return;
}
this.scene.addNewElement(imageElement);
this.scene.insertElement(imageElement);
try {
return await this.initializeImage({
@ -9792,7 +9778,9 @@ export const createTestHook = () => {
return this.app?.scene.getElementsIncludingDeleted();
},
set(elements: ExcalidrawElement[]) {
return this.app?.scene.replaceAllElements(elements);
return this.app?.scene.replaceAllElements(
syncInvalidIndices(elements),
);
},
},
});