fractionalIndex as a byproduct or zIndex
This commit is contained in:
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. */
|
||||||
|
1
src/tests/fixtures/elementFixture.ts
vendored
1
src/tests/fixtures/elementFixture.ts
vendored
@ -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:
|
||||||
|
125
src/zindex.ts
125
src/zindex.ts
@ -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…
x
Reference in New Issue
Block a user