diff --git a/src/scene/export.ts b/src/scene/export.ts index 54ede380c..51d653efb 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -59,7 +59,7 @@ const __createSceneForElementsHack__ = ( // ids to Scene instances so that we don't override the editor elements // mapping. // We still need to clone the objects themselves to regen references. - scene.replaceAllElements(cloneJSON(elements), false); + scene.replaceAllElements(cloneJSON(elements)); return scene; }; diff --git a/src/tests/fractionalIndex.test.ts b/src/tests/fractionalIndex.test.ts index 98b4d0169..25b58a70c 100644 --- a/src/tests/fractionalIndex.test.ts +++ b/src/tests/fractionalIndex.test.ts @@ -1,10 +1,13 @@ -import { nanoid } from "nanoid"; import { + fixFractionalIndices, restoreFractionalIndicies, validateFractionalIndicies, } from "../fractionalIndex"; import { ExcalidrawElement } from "../element/types"; import { API } from "./helpers/api"; +import { arrayToMap } from "../utils"; +import { moveAllLeft, moveOneLeft, moveOneRight } from "../zindex"; +import { AppState } from "../types"; const createElementWithIndex = ( fractionalIndex: string | null = null, @@ -15,6 +18,30 @@ const createElementWithIndex = ( }); }; +const testLengthAndOrder = ( + before: ExcalidrawElement[], + after: ExcalidrawElement[], +) => { + // length is not changed + expect(after.length).toBe(before.length); + // order is not changed + expect(after.map((e) => e.id)).deep.equal(before.map((e) => e.id)); +}; + +const testValidity = (elements: ExcalidrawElement[]) => { + expect(validateFractionalIndicies(elements)).toBe(true); +}; + +const genrateElementsAtLength = (length: number) => { + const elements: ExcalidrawElement[] = []; + + for (let i = 0; i < length; i++) { + elements.push(createElementWithIndex()); + } + + return elements; +}; + describe("restoring fractional indicies", () => { it("restore all null fractional indices", () => { const randomNumOfElements = Math.floor(Math.random() * 100); @@ -30,14 +57,8 @@ describe("restoring fractional indicies", () => { const restoredElements = restoreFractionalIndicies(elements); - // length is not changed - expect(restoredElements.length).toBe(randomNumOfElements); - // order is not changed - expect(restoredElements.map((e) => e.id)).deep.equal( - elements.map((e) => e.id), - ); - // fractional indices are valid - expect(validateFractionalIndicies(restoredElements)).toBe(true); + testLengthAndOrder(elements, restoredElements); + testValidity(restoredElements); }); it("restore out of order fractional indices", () => { @@ -50,14 +71,8 @@ describe("restoring fractional indicies", () => { const restoredElements = restoreFractionalIndicies(elements); - // length is not changed - expect(restoredElements.length).toBe(4); - // order is not changed - expect(restoredElements.map((e) => e.id)).deep.equal( - elements.map((e) => e.id), - ); - // fractional indices are valid - expect(validateFractionalIndicies(restoredElements)).toBe(true); + testLengthAndOrder(elements, restoredElements); + testValidity(restoredElements); // should only fix the second element's fractional index expect(elements[1].fractionalIndex).not.toEqual( restoredElements[1].fractionalIndex, @@ -81,14 +96,8 @@ describe("restoring fractional indicies", () => { const restoredElements = restoreFractionalIndicies(elements); - // length is not changed - expect(restoredElements.length).toBe(randomNumOfElements); - // order is not changed - expect(restoredElements.map((e) => e.id)).deep.equal( - elements.map((e) => e.id), - ); - // should've restored fractional indices properly - expect(validateFractionalIndicies(restoredElements)).toBe(true); + testLengthAndOrder(elements, restoredElements); + testValidity(restoredElements); expect(new Set(restoredElements.map((e) => e.fractionalIndex)).size).toBe( randomNumOfElements, ); @@ -108,16 +117,213 @@ describe("restoring fractional indicies", () => { const restoredElements = restoreFractionalIndicies(elements); - // length is not changed - expect(restoredElements.length).toBe(elements.length); - // order is not changed - expect(restoredElements.map((e) => e.id)).deep.equal( - elements.map((e) => e.id), - ); - // should've restored fractional indices properly - expect(validateFractionalIndicies(restoredElements)).toBe(true); + testLengthAndOrder(elements, restoredElements); + testValidity(restoredElements); expect(new Set(restoredElements.map((e) => e.fractionalIndex)).size).toBe( elements.length, ); }); }); + +describe("fix fractional indices", () => { + it("add each new element properly", () => { + const elements = [ + createElementWithIndex(), + createElementWithIndex(), + createElementWithIndex(), + createElementWithIndex(), + ]; + + const fixedElements = elements.reduce((acc, el) => { + return fixFractionalIndices([...acc, el], arrayToMap([el])); + }, [] as ExcalidrawElement[]); + + testLengthAndOrder(elements, fixedElements); + testValidity(fixedElements); + }); + + it("add multiple new elements properly", () => { + const elements = genrateElementsAtLength(Math.floor(Math.random() * 100)); + + const fixedElements = fixFractionalIndices(elements, arrayToMap(elements)); + + testLengthAndOrder(elements, fixedElements); + testValidity(fixedElements); + + const elements2 = genrateElementsAtLength(Math.floor(Math.random() * 100)); + + const allElements2 = [...elements, ...elements2]; + + const fixedElements2 = fixFractionalIndices( + allElements2, + arrayToMap(elements2), + ); + + testLengthAndOrder(allElements2, fixedElements2); + testValidity(fixedElements2); + }); + + it("fix properly after z-index changes", () => { + const elements = genrateElementsAtLength(Math.random() * 100); + + const fixedElements = fixFractionalIndices(elements, arrayToMap(elements)); + + let randomlySelected = [ + ...new Set([ + fixedElements[Math.floor(Math.random() * fixedElements.length)], + fixedElements[Math.floor(Math.random() * fixedElements.length)], + fixedElements[Math.floor(Math.random() * fixedElements.length)], + fixedElements[Math.floor(Math.random() * fixedElements.length)], + fixedElements[Math.floor(Math.random() * fixedElements.length)], + fixedElements[Math.floor(Math.random() * fixedElements.length)], + fixedElements[Math.floor(Math.random() * fixedElements.length)], + ]), + ]; + + const movedOneLeftFixedElements = moveOneLeft( + fixedElements, + randomlySelected.reduce( + (acc, el) => { + acc.selectedElementIds[el.id] = true; + return acc; + }, + { + selectedElementIds: {}, + } as { + selectedElementIds: Record; + }, + ) as any as AppState, + ); + + testValidity(movedOneLeftFixedElements); + + randomlySelected = [ + ...new Set([ + movedOneLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + ]), + ]; + + const movedOneRightFixedElements = moveOneRight( + movedOneLeftFixedElements, + randomlySelected.reduce( + (acc, el) => { + acc.selectedElementIds[el.id] = true; + return acc; + }, + { + selectedElementIds: {}, + } as { + selectedElementIds: Record; + }, + ) as any as AppState, + ); + + testValidity(movedOneRightFixedElements); + + randomlySelected = [ + ...new Set([ + movedOneRightFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneRightFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneRightFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneRightFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneRightFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneRightFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedOneRightFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + ]), + ]; + + const movedAllLeftFixedElements = moveAllLeft( + movedOneRightFixedElements, + randomlySelected.reduce( + (acc, el) => { + acc.selectedElementIds[el.id] = true; + return acc; + }, + { + selectedElementIds: {}, + } as { + selectedElementIds: Record; + }, + ) as any as AppState, + ) as ExcalidrawElement[]; + + testValidity(movedAllLeftFixedElements); + + randomlySelected = [ + ...new Set([ + movedAllLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedAllLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedAllLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedAllLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedAllLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedAllLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + movedAllLeftFixedElements[ + Math.floor(Math.random() * fixedElements.length) + ], + ]), + ]; + + const movedAllRightFixedElements = moveAllLeft( + movedAllLeftFixedElements, + randomlySelected.reduce( + (acc, el) => { + acc.selectedElementIds[el.id] = true; + return acc; + }, + { + selectedElementIds: {}, + } as { + selectedElementIds: Record; + }, + ) as any as AppState, + ) as ExcalidrawElement[]; + + testValidity(movedAllRightFixedElements); + }); +}); diff --git a/src/zindex.ts b/src/zindex.ts index 5fb133fb1..d5f90f4b1 100644 --- a/src/zindex.ts +++ b/src/zindex.ts @@ -1,4 +1,3 @@ -import { bumpVersion } from "./element/mutateElement"; import { isFrameLikeElement } from "./element/typeChecks"; import { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./element/types"; import { fixFractionalIndices } from "./fractionalIndex";