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",
"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",

View File

@ -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,

View File

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

View File

@ -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. */

View File

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

View File

@ -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:

View File

@ -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

View File

@ -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"