fractionalIndex as a byproduct or zIndex

mrazator/test-fractional-index-and-granular-history
Ryan Di 1 year ago
parent c7ee46e7f8
commit 02dc00a47e

@ -1,4 +1,3 @@
import React from "react";
import { import {
moveOneLeft, moveOneLeft,
moveOneRight, moveOneRight,

@ -26,7 +26,6 @@ import {
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN, DEFAULT_VERTICAL_ALIGN,
PRECEDING_ELEMENT_KEY,
FONT_FAMILY, FONT_FAMILY,
ROUNDNESS, ROUNDNESS,
DEFAULT_SIDEBAR, DEFAULT_SIDEBAR,
@ -44,6 +43,7 @@ import {
measureBaseline, measureBaseline,
} from "../element/textElement"; } from "../element/textElement";
import { normalizeLink } from "./url"; import { normalizeLink } from "./url";
import { generateConsistentFractionalIndex } from "../fractionalIndex";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@ -101,8 +101,6 @@ const restoreElementWithProperties = <
boundElementIds?: readonly ExcalidrawElement["id"][]; boundElementIds?: readonly ExcalidrawElement["id"][];
/** @deprecated */ /** @deprecated */
strokeSharpness?: StrokeRoundness; strokeSharpness?: StrokeRoundness;
/** metadata that may be present in elements during collaboration */
[PRECEDING_ELEMENT_KEY]?: string;
}, },
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>, K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>( >(
@ -115,14 +113,14 @@ const restoreElementWithProperties = <
> & > &
Partial<Pick<ExcalidrawElement, "type" | "x" | "y" | "customData">>, Partial<Pick<ExcalidrawElement, "type" | "x" | "y" | "customData">>,
): T => { ): T => {
const base: Pick<T, keyof ExcalidrawElement> & { const base: Pick<T, keyof ExcalidrawElement> = {
[PRECEDING_ELEMENT_KEY]?: string;
} = {
type: extra.type || element.type, type: extra.type || element.type,
// all elements must have version > 0 so getSceneVersion() will pick up // all elements must have version > 0 so getSceneVersion() will pick up
// newly added elements // newly added elements
version: element.version || 1, version: element.version || 1,
versionNonce: element.versionNonce ?? 0, versionNonce: element.versionNonce ?? 0,
// TODO: think about this more
fractionalIndex: element.fractionalIndex ?? Infinity,
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,
@ -166,10 +164,6 @@ const restoreElementWithProperties = <
"customData" in extra ? extra.customData : element.customData; "customData" in extra ? extra.customData : element.customData;
} }
if (PRECEDING_ELEMENT_KEY in element) {
base[PRECEDING_ELEMENT_KEY] = element[PRECEDING_ELEMENT_KEY];
}
return { return {
...base, ...base,
...getNormalizedDimensions(base), ...getNormalizedDimensions(base),
@ -589,7 +583,9 @@ export const restore = (
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean }, elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
): RestoredDataState => { ): RestoredDataState => {
return { return {
elements: restoreElements(data?.elements, localElements, elementsConfig), elements: generateConsistentFractionalIndex(
restoreElements(data?.elements, localElements, elementsConfig),
),
appState: restoreAppState(data?.appState, localAppState || null), appState: restoreAppState(data?.appState, localAppState || null),
files: data?.files || {}, files: data?.files || {},
}; };

@ -55,6 +55,7 @@ export type ElementConstructorOpts = MarkOptional<
| "angle" | "angle"
| "groupIds" | "groupIds"
| "frameId" | "frameId"
| "fractionalIndex"
| "boundElements" | "boundElements"
| "seed" | "seed"
| "version" | "version"
@ -88,6 +89,8 @@ const _newElementBase = <T extends ExcalidrawElement>(
angle = 0, angle = 0,
groupIds = [], groupIds = [],
frameId = null, frameId = null,
// TODO: think about this more
fractionalIndex = Infinity,
roundness = null, roundness = null,
boundElements = null, boundElements = null,
link = null, link = null,
@ -113,6 +116,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
opacity, opacity,
groupIds, groupIds,
frameId, frameId,
fractionalIndex,
roundness, roundness,
seed: rest.seed ?? randomInteger(), seed: rest.seed ?? randomInteger(),
version: rest.version || 1, version: rest.version || 1,

@ -50,6 +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;
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. */

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

@ -100,6 +100,7 @@ export class API {
id?: string; id?: string;
isDeleted?: boolean; isDeleted?: boolean;
frameId?: ExcalidrawElement["id"] | null; frameId?: ExcalidrawElement["id"] | null;
fractionalIndex: ExcalidrawElement["fractionalIndex"];
groupIds?: string[]; groupIds?: string[];
// generic element props // generic element props
strokeColor?: ExcalidrawGenericElement["strokeColor"]; strokeColor?: ExcalidrawGenericElement["strokeColor"];
@ -167,6 +168,7 @@ export class API {
x, x,
y, y,
frameId: rest.frameId ?? null, frameId: rest.frameId ?? null,
fractionalIndex: rest.fractionalIndex ?? Infinity,
angle: rest.angle ?? 0, angle: rest.angle ?? 0,
strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor, strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
backgroundColor: backgroundColor:

@ -485,6 +485,99 @@ function shiftElementsAccountingForFrames(
); );
} }
// fractional indexing
// -----------------------------------------------------------------------------
const FRACTIONAL_INDEX_FLOOR = 0;
const FRACTIONAL_INDEX_CEILING = 1;
const isFractionalIndexInValidRange = (index: number) => {
return index > FRACTIONAL_INDEX_FLOOR && index < FRACTIONAL_INDEX_CEILING;
};
const getFractionalIndex = (
element: ExcalidrawElement | undefined,
fallbackValue: number,
) => {
return element && isFractionalIndexInValidRange(element.fractionalIndex)
? element.fractionalIndex
: fallbackValue;
};
const isValidFractionalIndex = (
index: number,
predecessorElement: ExcalidrawElement | undefined,
successorElement: ExcalidrawElement | undefined,
) => {
return (
isFractionalIndexInValidRange(index) &&
index > getFractionalIndex(predecessorElement, FRACTIONAL_INDEX_FLOOR) &&
index < getFractionalIndex(successorElement, FRACTIONAL_INDEX_CEILING)
);
};
const randomNumInBetween = (start: number, end: number) => {
return Math.random() * (end - start) + start;
};
export const generateFractionalIndex = ({
start = FRACTIONAL_INDEX_FLOOR,
end = FRACTIONAL_INDEX_CEILING,
}: {
start?: number;
end?: number;
}) => {
const nextTemp = randomNumInBetween(start, end);
return (
(randomNumInBetween(nextTemp, end) + randomNumInBetween(start, nextTemp)) /
2
);
};
/**
* 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
* b. for every element, its fractional index is greater than its predecessor's and smaller than its successor's
*/
export const normalizeFractionalIndexing = (
allElements: readonly ExcalidrawElement[],
) => {
let predecessor = -1;
let successor = 1;
const normalizedElements: ExcalidrawElement[] = [];
for (const element of allElements) {
const predecessorElement = allElements[predecessor];
const successorElement = allElements[successor];
if (
!isValidFractionalIndex(
element.fractionalIndex,
predecessorElement,
successorElement,
)
) {
const nextFractionalIndex = generateFractionalIndex({
start: getFractionalIndex(predecessorElement, FRACTIONAL_INDEX_FLOOR),
end: getFractionalIndex(successorElement, FRACTIONAL_INDEX_CEILING),
});
normalizedElements.push({
...element,
fractionalIndex: nextFractionalIndex,
});
} else {
normalizedElements.push(element);
}
predecessor++;
successor++;
}
return normalizedElements;
};
// public API // public API
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -492,25 +585,31 @@ export const moveOneLeft = (
allElements: readonly ExcalidrawElement[], allElements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) => { ) => {
return shiftElementsByOne(allElements, appState, "left"); return normalizeFractionalIndexing(
shiftElementsByOne(allElements, appState, "left"),
);
}; };
export const moveOneRight = ( export const moveOneRight = (
allElements: readonly ExcalidrawElement[], allElements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) => { ) => {
return shiftElementsByOne(allElements, appState, "right"); return normalizeFractionalIndexing(
shiftElementsByOne(allElements, appState, "right"),
);
}; };
export const moveAllLeft = ( export const moveAllLeft = (
allElements: readonly ExcalidrawElement[], allElements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) => { ) => {
return shiftElementsAccountingForFrames( return normalizeFractionalIndexing(
allElements, shiftElementsAccountingForFrames(
appState, allElements,
"left", appState,
shiftElementsToEnd, "left",
shiftElementsToEnd,
),
); );
}; };
@ -518,10 +617,12 @@ export const moveAllRight = (
allElements: readonly ExcalidrawElement[], allElements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) => { ) => {
return shiftElementsAccountingForFrames( return normalizeFractionalIndexing(
allElements, shiftElementsAccountingForFrames(
appState, allElements,
"right", appState,
shiftElementsToEnd, "right",
shiftElementsToEnd,
),
); );
}; };

Loading…
Cancel
Save