From 00ffa08e2878d92d1518ae4f723706fbe7c0dff7 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Thu, 30 Nov 2023 19:02:14 +0800 Subject: [PATCH] use string as fractional index value --- package.json | 1 + src/data/restore.ts | 2 +- src/element/newElement.ts | 2 +- src/element/types.ts | 2 +- src/tests/fixtures/elementFixture.ts | 2 +- src/tests/helpers/api.ts | 2 +- src/zindex.ts | 163 +++++++++++++++------------ yarn.lock | 5 + 8 files changed, 103 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 4f8e550f3..abe130df4 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "eslint-plugin-react": "7.32.2", "fake-indexeddb": "3.1.7", "firebase": "8.3.3", + "fractional-indexing": "3.2.0", "i18next-browser-languagedetector": "6.1.4", "idb-keyval": "6.0.3", "image-blob-reduce": "3.0.1", diff --git a/src/data/restore.ts b/src/data/restore.ts index 1aa567e4a..e04f0b6c0 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -120,7 +120,7 @@ const restoreElementWithProperties = < version: element.version || 1, versionNonce: element.versionNonce ?? 0, // TODO: think about this more - fractionalIndex: element.fractionalIndex ?? Infinity, + fractionalIndex: element.fractionalIndex ?? null, isDeleted: element.isDeleted ?? false, id: element.id || randomId(), fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle, diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 8d5a323a7..82d7b8d9a 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -90,7 +90,7 @@ const _newElementBase = ( groupIds = [], frameId = null, // TODO: think about this more - fractionalIndex = Infinity, + fractionalIndex = null, roundness = null, boundElements = null, link = null, diff --git a/src/element/types.ts b/src/element/types.ts index 1c2cab972..38aa0c4bd 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -50,7 +50,7 @@ type _ExcalidrawElementBase = Readonly<{ Used for deterministic reconciliation of updates during collaboration, in case the versions (see above) are identical. */ versionNonce: number; - fractionalIndex: number; + fractionalIndex: string | null; isDeleted: boolean; /** List of groups the element belongs to. Ordered from deepest to shallowest. */ diff --git a/src/tests/fixtures/elementFixture.ts b/src/tests/fixtures/elementFixture.ts index 5adffbb2b..94cc4cea1 100644 --- a/src/tests/fixtures/elementFixture.ts +++ b/src/tests/fixtures/elementFixture.ts @@ -17,7 +17,7 @@ const elementBase: Omit = { groupIds: [], frameId: null, roundness: null, - fractionalIndex: Infinity, + fractionalIndex: "", seed: 1041657908, version: 120, versionNonce: 1188004276, diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index c327f5bc0..04ab69614 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -168,7 +168,7 @@ export class API { x, y, frameId: rest.frameId ?? null, - fractionalIndex: rest.fractionalIndex ?? Infinity, + fractionalIndex: rest.fractionalIndex ?? null, angle: rest.angle ?? 0, strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor, backgroundColor: diff --git a/src/zindex.ts b/src/zindex.ts index 214dba47d..b33aa6bd2 100644 --- a/src/zindex.ts +++ b/src/zindex.ts @@ -6,6 +6,7 @@ import { getSelectedElements } from "./scene"; import Scene from "./scene/Scene"; import { AppState } from "./types"; import { arrayToMap, findIndex, findLastIndex } from "./utils"; +import { generateKeyBetween } from "fractional-indexing"; const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => { return element.frameId === frameId || element.id === frameId; @@ -487,65 +488,78 @@ function shiftElementsAccountingForFrames( // fractional indexing // ----------------------------------------------------------------------------- -const FRACTIONAL_INDEX_FLOOR = 0; -const FRACTIONAL_INDEX_CEILING = 1; +type FractionalIndex = ExcalidrawElement["fractionalIndex"]; -const isFractionalIndexInValidRange = (index: number) => { - return index > FRACTIONAL_INDEX_FLOOR && index < FRACTIONAL_INDEX_CEILING; -}; +const fractionalIndexCompare = { + isSmallerThan(indexA: string, indexB: string) { + return indexA < indexB; + }, -const getFractionalIndex = ( - element: ExcalidrawElement | undefined, - fallbackValue: number, -) => { - return element && isFractionalIndexInValidRange(element.fractionalIndex) - ? element.fractionalIndex - : fallbackValue; + isGreaterThan(indexA: string, indexB: string) { + return indexA > indexB; + }, }; const isValidFractionalIndex = ( - index: number, - predecessorElement: ExcalidrawElement | undefined, - successorElement: ExcalidrawElement | undefined, + index: FractionalIndex, + predecessorFractionalIndex: FractionalIndex, + successorFractionalIndex: FractionalIndex, ) => { - return ( - isFractionalIndexInValidRange(index) && - index > getFractionalIndex(predecessorElement, FRACTIONAL_INDEX_FLOOR) && - index < getFractionalIndex(successorElement, FRACTIONAL_INDEX_CEILING) + if (index) { + if (predecessorFractionalIndex) { + if (successorFractionalIndex) { + return ( + fractionalIndexCompare.isGreaterThan( + index, + predecessorFractionalIndex, + ) && + fractionalIndexCompare.isSmallerThan(index, successorFractionalIndex) + ); + } + return fractionalIndexCompare.isGreaterThan( + index, + predecessorFractionalIndex, + ); + } + + if (successorFractionalIndex) { + return fractionalIndexCompare.isSmallerThan( + index, + successorFractionalIndex, + ); + } + + return index.length > 0; + } + + return false; +}; + +const generateFractionalIndex = ( + predecessorFractionalIndex: string | null, + successorFractionalIndex: string | null, +) => { + if (predecessorFractionalIndex && successorFractionalIndex) { + if (predecessorFractionalIndex < successorFractionalIndex) { + return generateKeyBetween( + predecessorFractionalIndex, + successorFractionalIndex, + ); + } else if (predecessorFractionalIndex > successorFractionalIndex) { + return generateKeyBetween( + successorFractionalIndex, + predecessorFractionalIndex, + ); + } + return generateKeyBetween(predecessorFractionalIndex, null); + } + + return generateKeyBetween( + predecessorFractionalIndex, + successorFractionalIndex, ); }; -const randomNumInBetween = (start: number, end: number) => { - return Math.random() * (end - start) + start; -}; - -export const generateFractionalIndex = ( - predecessorElement: ExcalidrawElement | undefined, - successorElement: ExcalidrawElement | undefined, -) => { - const start = getFractionalIndex(predecessorElement, FRACTIONAL_INDEX_FLOOR); - const end = getFractionalIndex(successorElement, FRACTIONAL_INDEX_CEILING); - - const nextTemp = randomNumInBetween(start, end); - return ( - (randomNumInBetween(nextTemp, end) + randomNumInBetween(start, nextTemp)) / - 2 - ); -}; - -/** - * - */ -export const getNextFractionalIndexAt = ( - index: number, - allElements: ExcalidrawElement[], -) => { - const predecessor = allElements[index - 1]; - const successor = allElements[index + 1]; - - return generateFractionalIndex(predecessor, successor); -}; - /** * normalize the fractional indicies of the elements in the given array such that * a. all elements have a fraction index between floor and ceiling as defined above @@ -557,37 +571,44 @@ export const normalizeFractionalIndexing = ( let predecessor = -1; let successor = 1; - const normalizedElements: ExcalidrawElement[] = []; + const normalizedElementsMap = arrayToMap(allElements); for (const element of allElements) { - const predecessorElement = allElements[predecessor]; - const successorElement = allElements[successor]; + const predecessorFractionalIndex = + normalizedElementsMap.get(allElements[predecessor]?.id) + ?.fractionalIndex || null; - if ( - !isValidFractionalIndex( - element.fractionalIndex, - predecessorElement, - successorElement, - ) - ) { - const nextFractionalIndex = generateFractionalIndex( - predecessorElement, - successorElement, - ); + const successorFractionalIndex = + normalizedElementsMap.get(allElements[successor]?.id)?.fractionalIndex || + null; - normalizedElements.push({ - ...element, - fractionalIndex: nextFractionalIndex, - }); - } else { - normalizedElements.push(element); + try { + if ( + !isValidFractionalIndex( + element.fractionalIndex, + predecessorFractionalIndex, + successorFractionalIndex, + ) + ) { + const nextFractionalIndex = generateFractionalIndex( + predecessorFractionalIndex, + successorFractionalIndex, + ); + + normalizedElementsMap.set(element.id, { + ...element, + fractionalIndex: nextFractionalIndex, + }); + } + } catch (e) { + console.error(e); } predecessor++; successor++; } - return normalizedElements; + return [...normalizedElementsMap.values()]; }; // public API diff --git a/yarn.lock b/yarn.lock index 82691f7b2..020fe51f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4891,6 +4891,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +fractional-indexing@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fractional-indexing/-/fractional-indexing-3.2.0.tgz#1193e63d54ff4e0cbe0c79a9ed6cfbab25d91628" + integrity sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ== + fs-extra@^11.1.0: version "11.1.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"