ソースを参照

fractionalIndex as a byproduct or zIndex

Ryan Di 1 年間 前
コミット
02dc00a47e

+ 0 - 1
src/actions/actionZindex.tsx

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

+ 7 - 11
src/data/restore.ts

@@ -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 || {},
   };

+ 4 - 0
src/element/newElement.ts

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

+ 1 - 0
src/element/types.ts

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

+ 1 - 0
src/tests/fixtures/elementFixture.ts

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

+ 2 - 0
src/tests/helpers/api.ts

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

+ 113 - 12
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
 // -----------------------------------------------------------------------------
 
@@ -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(
-    allElements,
-    appState,
-    "left",
-    shiftElementsToEnd,
+  return normalizeFractionalIndexing(
+    shiftElementsAccountingForFrames(
+      allElements,
+      appState,
+      "left",
+      shiftElementsToEnd,
+    ),
   );
 };
 
@@ -518,10 +617,12 @@ export const moveAllRight = (
   allElements: readonly ExcalidrawElement[],
   appState: AppState,
 ) => {
-  return shiftElementsAccountingForFrames(
-    allElements,
-    appState,
-    "right",
-    shiftElementsToEnd,
+  return normalizeFractionalIndexing(
+    shiftElementsAccountingForFrames(
+      allElements,
+      appState,
+      "right",
+      shiftElementsToEnd,
+    ),
   );
 };