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

@ -31,8 +31,9 @@ import {
} from "../element/types";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import { getFontString } from "../utils";
import { arrayToMap, getFontString } from "../utils";
import { register } from "./register";
import { syncMovedIndices } from "../fractionalIndex";
export const actionUnbindText = register({
name: "unbindText",
@ -180,6 +181,8 @@ const pushTextAboveContainer = (
(ele) => ele.id === container.id,
);
updatedElements.splice(containerIndex + 1, 0, textElement);
syncMovedIndices(updatedElements, arrayToMap([container, textElement]));
return updatedElements;
};
@ -198,6 +201,8 @@ const pushContainerBelowText = (
(ele) => ele.id === textElement.id,
);
updatedElements.splice(textElementIndex, 0, container);
syncMovedIndices(updatedElements, arrayToMap([container, textElement]));
return updatedElements;
};
@ -304,6 +309,7 @@ export const actionWrapTextInContainer = register({
container,
textElement,
);
containerIds[container.id] = true;
}
}

View file

@ -31,6 +31,7 @@ import {
excludeElementsInFramesFromSelection,
getSelectedElements,
} from "../scene/selection";
import { syncMovedIndices } from "../fractionalIndex";
export const actionDuplicateSelection = register({
name: "duplicateSelection",
@ -90,6 +91,7 @@ const duplicateElements = (
const newElements: ExcalidrawElement[] = [];
const oldElements: ExcalidrawElement[] = [];
const oldIdToDuplicatedId = new Map();
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
const duplicateAndOffsetElement = (element: ExcalidrawElement) => {
const newElement = duplicateElement(
@ -101,6 +103,7 @@ const duplicateElements = (
y: element.y + GRID_SIZE / 2,
},
);
duplicatedElementsMap.set(newElement.id, newElement);
oldIdToDuplicatedId.set(element.id, newElement.id);
oldElements.push(element);
newElements.push(newElement);
@ -238,9 +241,10 @@ const duplicateElements = (
}
// step (3)
const finalElements = finalElementsReversed.reverse();
syncMovedIndices(finalElements, arrayToMap([...oldElements, ...newElements]));
// ---------------------------------------------------------------------------
bindTextToShapeAfterDuplication(

View file

@ -27,6 +27,7 @@ import {
removeElementsFromFrame,
replaceAllElementsInFrame,
} from "../frame";
import { syncMovedIndices } from "../fractionalIndex";
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
if (elements.length >= 2) {
@ -140,11 +141,12 @@ export const actionGroup = register({
.filter(
(updatedElement) => !isElementInGroup(updatedElement, newGroupId),
);
nextElements = [
const reorderedElements = [
...elementsBeforeGroup,
...elementsInGroup,
...elementsAfterGroup,
];
syncMovedIndices(reorderedElements, arrayToMap(elementsInGroup));
return {
appState: {
@ -155,7 +157,7 @@ export const actionGroup = register({
getNonDeletedElements(nextElements),
),
},
elements: nextElements,
elements: reorderedElements,
commitToHistory: true,
};
},

View file

@ -10,6 +10,7 @@ import { newElementWith } from "../element/mutateElement";
import { fixBindingsAfterDeletion } from "../element/binding";
import { arrayToMap } from "../utils";
import { isWindows } from "../constants";
import { syncInvalidIndices } from "../fractionalIndex";
const writeData = (
prevElements: readonly ExcalidrawElement[],
@ -48,6 +49,8 @@ const writeData = (
),
);
fixBindingsAfterDeletion(elements, deletedElements);
// TODO: will be replaced in #7348
syncInvalidIndices(elements);
return {
elements,

View file

@ -1,4 +1,3 @@
import React from "react";
import {
moveOneLeft,
moveOneRight,