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

@ -55,6 +55,7 @@ export type ElementConstructorOpts = MarkOptional<
| "angle"
| "groupIds"
| "frameId"
| "index"
| "boundElements"
| "seed"
| "version"
@ -89,6 +90,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
angle = 0,
groupIds = [],
frameId = null,
index = null,
roundness = null,
boundElements = null,
link = null,
@ -114,6 +116,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
opacity,
groupIds,
frameId,
index,
roundness,
seed: rest.seed ?? randomInteger(),
version: rest.version || 1,

View file

@ -1454,7 +1454,7 @@ describe("textWysiwyg", () => {
strokeWidth: 2,
type: "rectangle",
updated: 1,
version: 1,
version: 2,
width: 610,
x: 15,
y: 25,

View file

@ -24,6 +24,7 @@ export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
export type FractionalIndex = string & { _brand: "franctionalIndex" };
type _ExcalidrawElementBase = Readonly<{
id: string;
@ -50,6 +51,11 @@ type _ExcalidrawElementBase = Readonly<{
Used for deterministic reconciliation of updates during collaboration,
in case the versions (see above) are identical. */
versionNonce: number;
/** String in a fractional form defined by https://github.com/rocicorp/fractional-indexing.
Used for ordering in multiplayer scenarios, such as during reconciliation or undo / redo.
Always kept in sync with the array order by `syncMovedIndices` and `syncInvalidIndices`.
Could be null, i.e. for new elements which were not yet assigned to the scene. */
index: FractionalIndex | null;
isDeleted: boolean;
/** List of groups the element belongs to.
Ordered from deepest to shallowest. */
@ -164,6 +170,12 @@ export type ExcalidrawElement =
| ExcalidrawIframeElement
| ExcalidrawEmbeddableElement;
export type Ordered<TElement extends ExcalidrawElement> = TElement & {
index: FractionalIndex;
};
export type OrderedExcalidrawElement = Ordered<ExcalidrawElement>;
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
isDeleted: boolean;
};
@ -275,7 +287,10 @@ export type NonDeletedElementsMap = Map<
* Map of all excalidraw Scene elements, including deleted.
* Not a subset. Use this type when you need access to current Scene elements.
*/
export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
export type SceneElementsMap = Map<
ExcalidrawElement["id"],
Ordered<ExcalidrawElement>
> &
MakeBrand<"SceneElementsMap">;
/**
@ -284,7 +299,7 @@ export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
*/
export type NonDeletedSceneElementsMap = Map<
ExcalidrawElement["id"],
NonDeletedExcalidrawElement
Ordered<NonDeletedExcalidrawElement>
> &
MakeBrand<"NonDeletedSceneElementsMap">;