use string as fractional index value

This commit is contained in:
Ryan Di 2023-11-30 19:02:14 +08:00
parent 5c1787bdf4
commit 00ffa08e28
8 changed files with 103 additions and 76 deletions

View File

@ -37,6 +37,7 @@
"eslint-plugin-react": "7.32.2", "eslint-plugin-react": "7.32.2",
"fake-indexeddb": "3.1.7", "fake-indexeddb": "3.1.7",
"firebase": "8.3.3", "firebase": "8.3.3",
"fractional-indexing": "3.2.0",
"i18next-browser-languagedetector": "6.1.4", "i18next-browser-languagedetector": "6.1.4",
"idb-keyval": "6.0.3", "idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1", "image-blob-reduce": "3.0.1",

View File

@ -120,7 +120,7 @@ const restoreElementWithProperties = <
version: element.version || 1, version: element.version || 1,
versionNonce: element.versionNonce ?? 0, versionNonce: element.versionNonce ?? 0,
// TODO: think about this more // TODO: think about this more
fractionalIndex: element.fractionalIndex ?? Infinity, fractionalIndex: element.fractionalIndex ?? null,
isDeleted: element.isDeleted ?? false, isDeleted: element.isDeleted ?? false,
id: element.id || randomId(), id: element.id || randomId(),
fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle, fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle,

View File

@ -90,7 +90,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
groupIds = [], groupIds = [],
frameId = null, frameId = null,
// TODO: think about this more // TODO: think about this more
fractionalIndex = Infinity, fractionalIndex = null,
roundness = null, roundness = null,
boundElements = null, boundElements = null,
link = null, link = null,

View File

@ -50,7 +50,7 @@ type _ExcalidrawElementBase = Readonly<{
Used for deterministic reconciliation of updates during collaboration, Used for deterministic reconciliation of updates during collaboration,
in case the versions (see above) are identical. */ in case the versions (see above) are identical. */
versionNonce: number; versionNonce: number;
fractionalIndex: number; fractionalIndex: string | null;
isDeleted: boolean; isDeleted: boolean;
/** List of groups the element belongs to. /** List of groups the element belongs to.
Ordered from deepest to shallowest. */ Ordered from deepest to shallowest. */

View File

@ -17,7 +17,7 @@ const elementBase: Omit<ExcalidrawElement, "type"> = {
groupIds: [], groupIds: [],
frameId: null, frameId: null,
roundness: null, roundness: null,
fractionalIndex: Infinity, fractionalIndex: "",
seed: 1041657908, seed: 1041657908,
version: 120, version: 120,
versionNonce: 1188004276, versionNonce: 1188004276,

View File

@ -168,7 +168,7 @@ export class API {
x, x,
y, y,
frameId: rest.frameId ?? null, frameId: rest.frameId ?? null,
fractionalIndex: rest.fractionalIndex ?? Infinity, fractionalIndex: rest.fractionalIndex ?? null,
angle: rest.angle ?? 0, angle: rest.angle ?? 0,
strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor, strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
backgroundColor: backgroundColor:

View File

@ -6,6 +6,7 @@ import { getSelectedElements } from "./scene";
import Scene from "./scene/Scene"; import Scene from "./scene/Scene";
import { AppState } from "./types"; import { AppState } from "./types";
import { arrayToMap, findIndex, findLastIndex } from "./utils"; import { arrayToMap, findIndex, findLastIndex } from "./utils";
import { generateKeyBetween } from "fractional-indexing";
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => { const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
return element.frameId === frameId || element.id === frameId; return element.frameId === frameId || element.id === frameId;
@ -487,65 +488,78 @@ function shiftElementsAccountingForFrames(
// fractional indexing // fractional indexing
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const FRACTIONAL_INDEX_FLOOR = 0; type FractionalIndex = ExcalidrawElement["fractionalIndex"];
const FRACTIONAL_INDEX_CEILING = 1;
const isFractionalIndexInValidRange = (index: number) => { const fractionalIndexCompare = {
return index > FRACTIONAL_INDEX_FLOOR && index < FRACTIONAL_INDEX_CEILING; isSmallerThan(indexA: string, indexB: string) {
}; return indexA < indexB;
},
const getFractionalIndex = ( isGreaterThan(indexA: string, indexB: string) {
element: ExcalidrawElement | undefined, return indexA > indexB;
fallbackValue: number, },
) => {
return element && isFractionalIndexInValidRange(element.fractionalIndex)
? element.fractionalIndex
: fallbackValue;
}; };
const isValidFractionalIndex = ( const isValidFractionalIndex = (
index: number, index: FractionalIndex,
predecessorElement: ExcalidrawElement | undefined, predecessorFractionalIndex: FractionalIndex,
successorElement: ExcalidrawElement | undefined, successorFractionalIndex: FractionalIndex,
) => { ) => {
return ( if (index) {
isFractionalIndexInValidRange(index) && if (predecessorFractionalIndex) {
index > getFractionalIndex(predecessorElement, FRACTIONAL_INDEX_FLOOR) && if (successorFractionalIndex) {
index < getFractionalIndex(successorElement, FRACTIONAL_INDEX_CEILING) 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 * 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 * 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 predecessor = -1;
let successor = 1; let successor = 1;
const normalizedElements: ExcalidrawElement[] = []; const normalizedElementsMap = arrayToMap(allElements);
for (const element of allElements) { for (const element of allElements) {
const predecessorElement = allElements[predecessor]; const predecessorFractionalIndex =
const successorElement = allElements[successor]; normalizedElementsMap.get(allElements[predecessor]?.id)
?.fractionalIndex || null;
if ( const successorFractionalIndex =
!isValidFractionalIndex( normalizedElementsMap.get(allElements[successor]?.id)?.fractionalIndex ||
element.fractionalIndex, null;
predecessorElement,
successorElement,
)
) {
const nextFractionalIndex = generateFractionalIndex(
predecessorElement,
successorElement,
);
normalizedElements.push({ try {
...element, if (
fractionalIndex: nextFractionalIndex, !isValidFractionalIndex(
}); element.fractionalIndex,
} else { predecessorFractionalIndex,
normalizedElements.push(element); successorFractionalIndex,
)
) {
const nextFractionalIndex = generateFractionalIndex(
predecessorFractionalIndex,
successorFractionalIndex,
);
normalizedElementsMap.set(element.id, {
...element,
fractionalIndex: nextFractionalIndex,
});
}
} catch (e) {
console.error(e);
} }
predecessor++; predecessor++;
successor++; successor++;
} }
return normalizedElements; return [...normalizedElementsMap.values()];
}; };
// public API // public API

View File

@ -4891,6 +4891,11 @@ form-data@^4.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" 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: fs-extra@^11.1.0:
version "11.1.1" version "11.1.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"