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 {
moveOneLeft,
moveOneRight,

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

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

@ -50,6 +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;
isDeleted: boolean;
/** List of groups the element belongs to.
Ordered from deepest to shallowest. */

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

@ -100,6 +100,7 @@ export class API {
id?: string;
isDeleted?: boolean;
frameId?: ExcalidrawElement["id"] | null;
fractionalIndex: ExcalidrawElement["fractionalIndex"];
groupIds?: string[];
// generic element props
strokeColor?: ExcalidrawGenericElement["strokeColor"];
@ -167,6 +168,7 @@ export class API {
x,
y,
frameId: rest.frameId ?? null,
fractionalIndex: rest.fractionalIndex ?? Infinity,
angle: rest.angle ?? 0,
strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
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
// -----------------------------------------------------------------------------
@ -492,25 +585,31 @@ export const moveOneLeft = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsByOne(allElements, appState, "left");
return normalizeFractionalIndexing(
shiftElementsByOne(allElements, appState, "left"),
);
};
export const moveOneRight = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsByOne(allElements, appState, "right");
return normalizeFractionalIndexing(
shiftElementsByOne(allElements, appState, "right"),
);
};
export const moveAllLeft = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsAccountingForFrames(
return normalizeFractionalIndexing(
shiftElementsAccountingForFrames(
allElements,
appState,
"left",
shiftElementsToEnd,
),
);
};
@ -518,10 +617,12 @@ export const moveAllRight = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsAccountingForFrames(
return normalizeFractionalIndexing(
shiftElementsAccountingForFrames(
allElements,
appState,
"right",
shiftElementsToEnd,
),
);
};

Loading…
Cancel
Save