From bf53d90c686356116cab10637d32d38f3e7f1eb9 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Wed, 6 Dec 2023 23:25:11 +0800 Subject: [PATCH] indices with jitter --- package.json | 2 +- src/data/restore.ts | 4 +- src/fractionalIndex.ts | 127 ++++++++++++++++++++++++++--------------- src/scene/Scene.ts | 11 +++- yarn.lock | 8 +-- 5 files changed, 99 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index abe130df4..1572e9b14 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "eslint-plugin-react": "7.32.2", "fake-indexeddb": "3.1.7", "firebase": "8.3.3", - "fractional-indexing": "3.2.0", + "fractional-indexing-jittered": "0.9.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 3e865da18..1d5a465a8 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -43,7 +43,7 @@ import { measureBaseline, } from "../element/textElement"; import { normalizeLink } from "./url"; -import { normalizeFractionalIndicies } from "../fractionalIndex"; +import { restoreFractionalIndicies } from "../fractionalIndex"; type RestoredAppState = Omit< AppState, @@ -461,7 +461,7 @@ export const restoreElements = ( } } - return normalizeFractionalIndicies(restoredElements) as ExcalidrawElement[]; + return restoreFractionalIndicies(restoredElements) as ExcalidrawElement[]; }; const coalesceAppStateValue = < diff --git a/src/fractionalIndex.ts b/src/fractionalIndex.ts index 076dd9e95..29bd0a188 100644 --- a/src/fractionalIndex.ts +++ b/src/fractionalIndex.ts @@ -1,6 +1,9 @@ import { mutateElement } from "./element/mutateElement"; import { ExcalidrawElement } from "./element/types"; -import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing"; +import { + generateKeyBetween, + generateNJitteredKeysBetween, +} from "fractional-indexing-jittered"; type FractionalIndex = ExcalidrawElement["fractionalIndex"]; @@ -10,19 +13,23 @@ const isValidFractionalIndex = ( successor: FractionalIndex, ) => { if (index) { + if (predecessor && successor) { + return predecessor < index && index < successor; + } + + if (successor && !predecessor) { + // first element + return index < successor; + } + + if (predecessor && !successor) { + // last element + return predecessor < index; + } + if (!predecessor && !successor) { return index.length > 0; } - - if (!predecessor) { - // first element - return index < successor!; - } - - if (!successor) { - // last element - return predecessor! < index; - } } return false; @@ -89,7 +96,7 @@ export const fixFractionalIndices = ( elements[movedIndices[movedIndices.length - 1] + 1]?.fractionalIndex || null; - const newKeys = generateNKeysBetween( + const newKeys = generateNJitteredKeysBetween( predecessor, successor, movedIndices.length, @@ -114,36 +121,6 @@ export const fixFractionalIndices = ( return elements as ExcalidrawElement[]; }; -const generateFractionalIndex = ( - index: FractionalIndex, - predecessor: FractionalIndex, - successor: FractionalIndex, -) => { - if (index) { - if (!predecessor && !successor) { - return index; - } - - if (!predecessor) { - // first element in the array - // insert before successor - return generateKeyBetween(null, successor); - } - - if (!successor) { - // last element in the array - // insert after predecessor - return generateKeyBetween(predecessor, null); - } - - // both predecessor and successor exist - // insert after predecessor - return generateKeyBetween(predecessor, null); - } - - return generateKeyBetween(null, null); -}; - const compareStrings = (a: string, b: string) => { return a < b ? -1 : 1; }; @@ -163,6 +140,36 @@ export const orderByFractionalIndex = (allElements: ExcalidrawElement[]) => { }); }; +const restoreFractionalIndex = ( + index: FractionalIndex, + predecessor: FractionalIndex, + successor: FractionalIndex, +) => { + if (index) { + if (!predecessor && !successor) { + return index; + } + + if (successor && !predecessor) { + // first element in the array + // insert before successor + return generateKeyBetween(null, successor); + } + + if (predecessor && !successor) { + // last element in the array + // insert after predecessor + return generateKeyBetween(predecessor, null); + } + + // both predecessor and successor exist + // insert after predecessor + return generateKeyBetween(predecessor, null); + } + + return generateKeyBetween(null, null); +}; + /** * normalize the fractional indicies of the elements in the given array such that * every element in the array has a fractional index smaller than its successor's @@ -170,7 +177,7 @@ export const orderByFractionalIndex = (allElements: ExcalidrawElement[]) => { * note that this function is not pure, it mutates elements whose fractional indicies * need updating */ -export const normalizeFractionalIndicies = ( +export const restoreFractionalIndicies = ( allElements: readonly ExcalidrawElement[], ) => { let pre = -1; @@ -186,7 +193,7 @@ export const normalizeFractionalIndicies = ( !isValidFractionalIndex(element.fractionalIndex, predecessor, successor) ) { try { - const nextFractionalIndex = generateFractionalIndex( + const nextFractionalIndex = restoreFractionalIndex( element.fractionalIndex, predecessor, successor, @@ -197,7 +204,6 @@ export const normalizeFractionalIndicies = ( fractionalIndex: nextFractionalIndex, }); } catch (e) { - console.error("normalizing fractional index", e); normalized.push(element); } } else { @@ -209,3 +215,34 @@ export const normalizeFractionalIndicies = ( return normalized; }; + +export const validateFractionalIndicies = ( + elements: readonly ExcalidrawElement[], +) => { + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const successor = elements[i + 1]; + + if (successor) { + if (element.fractionalIndex && successor.fractionalIndex) { + if (element.fractionalIndex >= successor.fractionalIndex) { + console.log( + "this is the case", + element.fractionalIndex, + successor.fractionalIndex, + ); + return false; + } + } else { + console.log( + "this is the other case", + element.fractionalIndex, + successor.fractionalIndex, + ); + return false; + } + } + } + + return true; +}; diff --git a/src/scene/Scene.ts b/src/scene/Scene.ts index dca1e02c4..e60786fc5 100644 --- a/src/scene/Scene.ts +++ b/src/scene/Scene.ts @@ -11,7 +11,10 @@ import { getSelectedElements } from "./selection"; import { AppState } from "../types"; import { Assert, SameType } from "../utility-types"; import { randomInteger } from "../random"; -import { fixFractionalIndices } from "../fractionalIndex"; +import { + fixFractionalIndices, + validateFractionalIndicies, +} from "../fractionalIndex"; import { arrayToMap } from "../utils"; type ElementIdKey = InstanceType["elementId"]; @@ -240,6 +243,12 @@ class Scene { _nextElements = nextElements; } + if (import.meta.env.DEV) { + if (!validateFractionalIndicies(_nextElements)) { + console.error("fractional indices consistency has been compromised"); + } + } + this.elements = _nextElements; const nextFrameLikes: ExcalidrawFrameLikeElement[] = []; this.elementsMap.clear(); diff --git a/yarn.lock b/yarn.lock index 020fe51f8..4fab90ed3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4891,10 +4891,10 @@ 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== +fractional-indexing-jittered@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/fractional-indexing-jittered/-/fractional-indexing-jittered-0.9.0.tgz#53a5f05acc4a8f8ceb5cb5a326122deb2cf8105c" + integrity sha512-3XIGQbmuEIg1j/qdHLDVyCH1vNMUyeBhM+5d6Su03fTiHzmQLE5nOqvDXLDgg240lLBIsLyJa3xZIUqO57yrAQ== fs-extra@^11.1.0: version "11.1.1"