From bb58effebf3ea18ed2d6bd6ae815f022ea100fad Mon Sep 17 00:00:00 2001 From: dwelle <5153846+dwelle@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:45:04 +0100 Subject: [PATCH] [pr] fractional indices --- excalidraw-app/App.tsx | 11 +- excalidraw-app/collab/Collab.tsx | 49 +- excalidraw-app/collab/Portal.tsx | 37 +- excalidraw-app/collab/reconciliation.ts | 154 -- excalidraw-app/data/firebase.ts | 57 +- excalidraw-app/data/index.ts | 13 +- excalidraw-app/tests/collab.test.tsx | 6 +- excalidraw-app/tests/reconciliation.test.ts | 421 ---- .../excalidraw/actions/actionBoundText.tsx | 8 +- .../actions/actionDuplicateSelection.tsx | 6 +- packages/excalidraw/actions/actionGroup.tsx | 6 +- packages/excalidraw/actions/actionHistory.tsx | 3 + packages/excalidraw/actions/actionZindex.tsx | 1 - packages/excalidraw/components/App.tsx | 68 +- packages/excalidraw/constants.ts | 4 - .../data/__snapshots__/transform.test.ts.snap | 150 +- packages/excalidraw/data/reconcile.ts | 79 + packages/excalidraw/data/restore.ts | 59 +- packages/excalidraw/data/transform.ts | 16 +- packages/excalidraw/element/newElement.ts | 3 + .../excalidraw/element/textWysiwyg.test.tsx | 2 +- packages/excalidraw/element/types.ts | 19 +- packages/excalidraw/errors.ts | 4 + packages/excalidraw/fractionalIndex.ts | 348 ++++ packages/excalidraw/frame.ts | 2 +- packages/excalidraw/package.json | 5 +- packages/excalidraw/scene/Scene.ts | 60 +- packages/excalidraw/scene/export.ts | 3 +- .../__snapshots__/contextmenu.test.tsx.snap | 535 ++--- .../__snapshots__/dragCreate.test.tsx.snap | 25 +- .../tests/__snapshots__/move.test.tsx.snap | 36 +- .../multiPointCreate.test.tsx.snap | 10 +- .../regressionTests.test.tsx.snap | 1714 ++++++++++------- .../__snapshots__/selection.test.tsx.snap | 25 +- .../excalidraw/tests/contextmenu.test.tsx | 22 +- .../data/__snapshots__/restore.test.ts.snap | 43 +- .../excalidraw/tests/data/reconcile.test.ts | 374 ++++ .../excalidraw/tests/data/restore.test.ts | 46 +- .../tests/fixtures/elementFixture.ts | 1 + packages/excalidraw/tests/flip.test.tsx | 32 +- .../excalidraw/tests/fractionalIndex.test.ts | 774 ++++++++ packages/excalidraw/tests/helpers/api.ts | 2 + packages/excalidraw/tests/library.test.tsx | 7 +- .../excalidraw/tests/regressionTests.test.tsx | 2 +- .../scene/__snapshots__/export.test.ts.snap | 2 +- .../excalidraw/tests/scene/export.test.ts | 14 +- packages/excalidraw/tests/zindex.test.tsx | 1 + packages/excalidraw/types.ts | 3 +- packages/excalidraw/zindex.ts | 53 +- yarn.lock | 12 +- 50 files changed, 3460 insertions(+), 1867 deletions(-) delete mode 100644 excalidraw-app/collab/reconciliation.ts delete mode 100644 excalidraw-app/tests/reconciliation.test.ts create mode 100644 packages/excalidraw/data/reconcile.ts create mode 100644 packages/excalidraw/fractionalIndex.ts create mode 100644 packages/excalidraw/tests/data/reconcile.test.ts create mode 100644 packages/excalidraw/tests/fractionalIndex.test.ts diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index f846c6fb1da34..465082451fcb7 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -14,9 +14,9 @@ import { } from "../packages/excalidraw/constants"; import { loadFromBlob } from "../packages/excalidraw/data/blob"; import { - ExcalidrawElement, FileId, NonDeletedExcalidrawElement, + OrderedExcalidrawElement, Theme, } from "../packages/excalidraw/element/types"; import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState"; @@ -90,7 +90,6 @@ import { } from "./data/LocalData"; import { isBrowserStorageStateNewer } from "./data/tabSync"; import clsx from "clsx"; -import { reconcileElements } from "./collab/reconciliation"; import { parseLibraryTokensFromUrl, useHandleLibrary, @@ -109,6 +108,10 @@ import { OverwriteConfirmDialog } from "../packages/excalidraw/components/Overwr import Trans from "../packages/excalidraw/components/Trans"; import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog"; import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError"; +import { + RemoteExcalidrawElement, + reconcileElements, +} from "../packages/excalidraw/data/reconcile"; polyfill(); @@ -257,7 +260,7 @@ const initializeScene = async (opts: { }, elements: reconcileElements( scene?.elements || [], - excalidrawAPI.getSceneElementsIncludingDeleted(), + excalidrawAPI.getSceneElementsIncludingDeleted() as RemoteExcalidrawElement[], excalidrawAPI.getAppState(), ), }, @@ -569,7 +572,7 @@ const ExcalidrawWrapper = () => { }, [theme]); const onChange = ( - elements: readonly ExcalidrawElement[], + elements: readonly OrderedExcalidrawElement[], appState: AppState, files: BinaryFiles, ) => { diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index f7879c64ec124..def3810f90a17 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -10,6 +10,7 @@ import { ImportedDataState } from "../../packages/excalidraw/data/types"; import { ExcalidrawElement, InitializedExcalidrawImageElement, + OrderedExcalidrawElement, } from "../../packages/excalidraw/element/types"; import { getSceneVersion, @@ -69,10 +70,6 @@ import { isInitializedImageElement, } from "../../packages/excalidraw/element/typeChecks"; import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; -import { - ReconciledElements, - reconcileElements as _reconcileElements, -} from "./reconciliation"; import { decryptData } from "../../packages/excalidraw/data/encryption"; import { resetBrowserStateVersions } from "../data/tabSync"; import { LocalData } from "../data/LocalData"; @@ -82,6 +79,11 @@ import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types"; import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds"; import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils"; import { collabErrorIndicatorAtom } from "./CollabError"; +import { + ReconciledExcalidrawElement, + RemoteExcalidrawElement, + reconcileElements, +} from "../../packages/excalidraw/data/reconcile"; export const collabAPIAtom = atom(null); export const isCollaboratingAtom = atom(false); @@ -274,7 +276,7 @@ class Collab extends PureComponent { syncableElements: readonly SyncableExcalidrawElement[], ) => { try { - const savedData = await saveToFirebase( + const storedElements = await saveToFirebase( this.portal, syncableElements, this.excalidrawAPI.getAppState(), @@ -282,10 +284,8 @@ class Collab extends PureComponent { this.resetErrorIndicator(); - if (this.isCollaborating() && savedData && savedData.reconciledElements) { - this.handleRemoteSceneUpdate( - this.reconcileElements(savedData.reconciledElements), - ); + if (this.isCollaborating() && storedElements) { + this.handleRemoteSceneUpdate(this._reconcileElements(storedElements)); } } catch (error: any) { const errorMessage = /is longer than.*?bytes/.test(error.message) @@ -429,7 +429,7 @@ class Collab extends PureComponent { startCollaboration = async ( existingRoomLinkData: null | { roomId: string; roomKey: string }, - ): Promise => { + ) => { if (!this.state.username) { import("@excalidraw/random-username").then(({ getRandomUsername }) => { const username = getRandomUsername(); @@ -455,7 +455,11 @@ class Collab extends PureComponent { ); } - const scenePromise = resolvablePromise(); + // TODO: `ImportedDataState` type here seems abused + const scenePromise = resolvablePromise< + | (ImportedDataState & { elements: readonly OrderedExcalidrawElement[] }) + | null + >(); this.setIsCollaborating(true); LocalData.pauseSave("collaboration"); @@ -538,7 +542,8 @@ class Collab extends PureComponent { if (!this.portal.socketInitialized) { this.initializeRoom({ fetchScene: false }); const remoteElements = decryptedData.payload.elements; - const reconciledElements = this.reconcileElements(remoteElements); + const reconciledElements = + this._reconcileElements(remoteElements); this.handleRemoteSceneUpdate(reconciledElements, { init: true, }); @@ -552,7 +557,7 @@ class Collab extends PureComponent { } case WS_SUBTYPES.UPDATE: this.handleRemoteSceneUpdate( - this.reconcileElements(decryptedData.payload.elements), + this._reconcileElements(decryptedData.payload.elements), ); break; case WS_SUBTYPES.MOUSE_LOCATION: { @@ -700,17 +705,15 @@ class Collab extends PureComponent { return null; }; - private reconcileElements = ( + private _reconcileElements = ( remoteElements: readonly ExcalidrawElement[], - ): ReconciledElements => { + ): ReconciledExcalidrawElement[] => { const localElements = this.getSceneElementsIncludingDeleted(); const appState = this.excalidrawAPI.getAppState(); - - remoteElements = restoreElements(remoteElements, null); - - const reconciledElements = _reconcileElements( + const restoredRemoteElements = restoreElements(remoteElements, null); + const reconciledElements = reconcileElements( localElements, - remoteElements, + restoredRemoteElements as RemoteExcalidrawElement[], appState, ); @@ -741,7 +744,7 @@ class Collab extends PureComponent { }, LOAD_IMAGES_TIMEOUT); private handleRemoteSceneUpdate = ( - elements: ReconciledElements, + elements: ReconciledExcalidrawElement[], { init = false }: { init?: boolean } = {}, ) => { this.excalidrawAPI.updateScene({ @@ -887,7 +890,7 @@ class Collab extends PureComponent { this.portal.broadcastIdleChange(userState); }; - broadcastElements = (elements: readonly ExcalidrawElement[]) => { + broadcastElements = (elements: readonly OrderedExcalidrawElement[]) => { if ( getSceneVersion(elements) > this.getLastBroadcastedOrReceivedSceneVersion() @@ -898,7 +901,7 @@ class Collab extends PureComponent { } }; - syncElements = (elements: readonly ExcalidrawElement[]) => { + syncElements = (elements: readonly OrderedExcalidrawElement[]) => { this.broadcastElements(elements); this.queueSaveToFirebase(); }; diff --git a/excalidraw-app/collab/Portal.tsx b/excalidraw-app/collab/Portal.tsx index bf8ffa5de09fe..8b5e5680e6208 100644 --- a/excalidraw-app/collab/Portal.tsx +++ b/excalidraw-app/collab/Portal.tsx @@ -2,11 +2,12 @@ import { isSyncableElement, SocketUpdateData, SocketUpdateDataSource, + SyncableExcalidrawElement, } from "../data"; import { TCollabClass } from "./Collab"; -import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; +import { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types"; import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants"; import { OnUserFollowedPayload, @@ -16,9 +17,7 @@ import { import { trackEvent } from "../../packages/excalidraw/analytics"; import throttle from "lodash.throttle"; import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; -import { BroadcastedExcalidrawElement } from "./reconciliation"; import { encryptData } from "../../packages/excalidraw/data/encryption"; -import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants"; import type { Socket } from "socket.io-client"; class Portal { @@ -133,7 +132,7 @@ class Portal { broadcastScene = async ( updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE, - allElements: readonly ExcalidrawElement[], + elements: readonly OrderedExcalidrawElement[], syncAll: boolean, ) => { if (updateType === WS_SUBTYPES.INIT && !syncAll) { @@ -143,25 +142,17 @@ class Portal { // sync out only the elements we think we need to to save bandwidth. // periodically we'll resync the whole thing to make sure no one diverges // due to a dropped message (server goes down etc). - const syncableElements = allElements.reduce( - (acc, element: BroadcastedExcalidrawElement, idx, elements) => { - if ( - (syncAll || - !this.broadcastedElementVersions.has(element.id) || - element.version > - this.broadcastedElementVersions.get(element.id)!) && - isSyncableElement(element) - ) { - acc.push({ - ...element, - // z-index info for the reconciler - [PRECEDING_ELEMENT_KEY]: idx === 0 ? "^" : elements[idx - 1]?.id, - }); - } - return acc; - }, - [] as BroadcastedExcalidrawElement[], - ); + const syncableElements = elements.reduce((acc, element) => { + if ( + (syncAll || + !this.broadcastedElementVersions.has(element.id) || + element.version > this.broadcastedElementVersions.get(element.id)!) && + isSyncableElement(element) + ) { + acc.push(element); + } + return acc; + }, [] as SyncableExcalidrawElement[]); const data: SocketUpdateDataSource[typeof updateType] = { type: updateType, diff --git a/excalidraw-app/collab/reconciliation.ts b/excalidraw-app/collab/reconciliation.ts deleted file mode 100644 index 15e17ed42899e..0000000000000 --- a/excalidraw-app/collab/reconciliation.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants"; -import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; -import { AppState } from "../../packages/excalidraw/types"; -import { arrayToMapWithIndex } from "../../packages/excalidraw/utils"; - -export type ReconciledElements = readonly ExcalidrawElement[] & { - _brand: "reconciledElements"; -}; - -export type BroadcastedExcalidrawElement = ExcalidrawElement & { - [PRECEDING_ELEMENT_KEY]?: string; -}; - -const shouldDiscardRemoteElement = ( - localAppState: AppState, - local: ExcalidrawElement | undefined, - remote: BroadcastedExcalidrawElement, -): boolean => { - if ( - local && - // local element is being edited - (local.id === localAppState.editingElement?.id || - local.id === localAppState.resizingElement?.id || - local.id === localAppState.draggingElement?.id || - // local element is newer - local.version > remote.version || - // resolve conflicting edits deterministically by taking the one with - // the lowest versionNonce - (local.version === remote.version && - local.versionNonce < remote.versionNonce)) - ) { - return true; - } - return false; -}; - -export const reconcileElements = ( - localElements: readonly ExcalidrawElement[], - remoteElements: readonly BroadcastedExcalidrawElement[], - localAppState: AppState, -): ReconciledElements => { - const localElementsData = - arrayToMapWithIndex(localElements); - - const reconciledElements: ExcalidrawElement[] = localElements.slice(); - - const duplicates = new WeakMap(); - - let cursor = 0; - let offset = 0; - - let remoteElementIdx = -1; - for (const remoteElement of remoteElements) { - remoteElementIdx++; - - const local = localElementsData.get(remoteElement.id); - - if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) { - if (remoteElement[PRECEDING_ELEMENT_KEY]) { - delete remoteElement[PRECEDING_ELEMENT_KEY]; - } - - continue; - } - - // Mark duplicate for removal as it'll be replaced with the remote element - if (local) { - // Unless the remote and local elements are the same element in which case - // we need to keep it as we'd otherwise discard it from the resulting - // array. - if (local[0] === remoteElement) { - continue; - } - duplicates.set(local[0], true); - } - - // parent may not be defined in case the remote client is running an older - // excalidraw version - const parent = - remoteElement[PRECEDING_ELEMENT_KEY] || - remoteElements[remoteElementIdx - 1]?.id || - null; - - if (parent != null) { - delete remoteElement[PRECEDING_ELEMENT_KEY]; - - // ^ indicates the element is the first in elements array - if (parent === "^") { - offset++; - if (cursor === 0) { - reconciledElements.unshift(remoteElement); - localElementsData.set(remoteElement.id, [ - remoteElement, - cursor - offset, - ]); - } else { - reconciledElements.splice(cursor + 1, 0, remoteElement); - localElementsData.set(remoteElement.id, [ - remoteElement, - cursor + 1 - offset, - ]); - cursor++; - } - } else { - let idx = localElementsData.has(parent) - ? localElementsData.get(parent)![1] - : null; - if (idx != null) { - idx += offset; - } - if (idx != null && idx >= cursor) { - reconciledElements.splice(idx + 1, 0, remoteElement); - offset++; - localElementsData.set(remoteElement.id, [ - remoteElement, - idx + 1 - offset, - ]); - cursor = idx + 1; - } else if (idx != null) { - reconciledElements.splice(cursor + 1, 0, remoteElement); - offset++; - localElementsData.set(remoteElement.id, [ - remoteElement, - cursor + 1 - offset, - ]); - cursor++; - } else { - reconciledElements.push(remoteElement); - localElementsData.set(remoteElement.id, [ - remoteElement, - reconciledElements.length - 1 - offset, - ]); - } - } - // no parent z-index information, local element exists → replace in place - } else if (local) { - reconciledElements[local[1]] = remoteElement; - localElementsData.set(remoteElement.id, [remoteElement, local[1]]); - // otherwise push to the end - } else { - reconciledElements.push(remoteElement); - localElementsData.set(remoteElement.id, [ - remoteElement, - reconciledElements.length - 1 - offset, - ]); - } - } - - const ret: readonly ExcalidrawElement[] = reconciledElements.filter( - (element) => !duplicates.has(element), - ); - - return ret as ReconciledElements; -}; diff --git a/excalidraw-app/data/firebase.ts b/excalidraw-app/data/firebase.ts index f37fbbd81cf9c..b0777d6d9d118 100644 --- a/excalidraw-app/data/firebase.ts +++ b/excalidraw-app/data/firebase.ts @@ -1,6 +1,7 @@ import { ExcalidrawElement, FileId, + OrderedExcalidrawElement, } from "../../packages/excalidraw/element/types"; import { getSceneVersion } from "../../packages/excalidraw/element"; import Portal from "../collab/Portal"; @@ -18,10 +19,13 @@ import { decryptData, } from "../../packages/excalidraw/data/encryption"; import { MIME_TYPES } from "../../packages/excalidraw/constants"; -import { reconcileElements } from "../collab/reconciliation"; import { getSyncableElements, SyncableExcalidrawElement } from "."; import { ResolutionType } from "../../packages/excalidraw/utility-types"; import type { Socket } from "socket.io-client"; +import { + RemoteExcalidrawElement, + reconcileElements, +} from "../../packages/excalidraw/data/reconcile"; // private // ----------------------------------------------------------------------------- @@ -230,7 +234,7 @@ export const saveToFirebase = async ( !socket || isSavedToFirebase(portal, elements) ) { - return false; + return null; } const firebase = await loadFirestore(); @@ -238,56 +242,59 @@ export const saveToFirebase = async ( const docRef = firestore.collection("scenes").doc(roomId); - const savedData = await firestore.runTransaction(async (transaction) => { + const storedScene = await firestore.runTransaction(async (transaction) => { const snapshot = await transaction.get(docRef); if (!snapshot.exists) { - const sceneDocument = await createFirebaseSceneDocument( + const storedScene = await createFirebaseSceneDocument( firebase, elements, roomKey, ); - transaction.set(docRef, sceneDocument); + transaction.set(docRef, storedScene); - return { - elements, - reconciledElements: null, - }; + return storedScene; } - const prevDocData = snapshot.data() as FirebaseStoredScene; - const prevElements = getSyncableElements( - await decryptElements(prevDocData, roomKey), + const prevStoredScene = snapshot.data() as FirebaseStoredScene; + const prevStoredElements = getSyncableElements( + restoreElements(await decryptElements(prevStoredScene, roomKey), null), ); - const reconciledElements = getSyncableElements( - reconcileElements(elements, prevElements, appState), + reconcileElements( + elements, + prevStoredElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[], + appState, + ), ); - const sceneDocument = await createFirebaseSceneDocument( + const storedScene = await createFirebaseSceneDocument( firebase, reconciledElements, roomKey, ); - transaction.update(docRef, sceneDocument); - return { - elements, - reconciledElements, - }; + transaction.update(docRef, storedScene); + + // Return the stored elements as the in memory `reconciledElements` could have mutated in the meantime + return storedScene; }); - FirebaseSceneVersionCache.set(socket, savedData.elements); + const storedElements = getSyncableElements( + restoreElements(await decryptElements(storedScene, roomKey), null), + ); + + FirebaseSceneVersionCache.set(socket, storedElements); - return { reconciledElements: savedData.reconciledElements }; + return storedElements; }; export const loadFromFirebase = async ( roomId: string, roomKey: string, socket: Socket | null, -): Promise => { +): Promise => { const firebase = await loadFirestore(); const db = firebase.firestore(); @@ -298,14 +305,14 @@ export const loadFromFirebase = async ( } const storedScene = doc.data() as FirebaseStoredScene; const elements = getSyncableElements( - await decryptElements(storedScene, roomKey), + restoreElements(await decryptElements(storedScene, roomKey), null), ); if (socket) { FirebaseSceneVersionCache.set(socket, elements); } - return restoreElements(elements, null); + return elements; }; export const loadFilesFromFirebase = async ( diff --git a/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts index 5699568b43d47..10c97fd205e3f 100644 --- a/excalidraw-app/data/index.ts +++ b/excalidraw-app/data/index.ts @@ -16,6 +16,7 @@ import { isInitializedImageElement } from "../../packages/excalidraw/element/typ import { ExcalidrawElement, FileId, + OrderedExcalidrawElement, } from "../../packages/excalidraw/element/types"; import { t } from "../../packages/excalidraw/i18n"; import { @@ -25,6 +26,7 @@ import { SocketId, UserIdleState, } from "../../packages/excalidraw/types"; +import { MakeBrand } from "../../packages/excalidraw/utility-types"; import { bytesToHexString } from "../../packages/excalidraw/utils"; import { DELETED_ELEMENT_TIMEOUT, @@ -35,12 +37,11 @@ import { import { encodeFilesForUpload } from "./FileManager"; import { saveFilesToFirebase } from "./firebase"; -export type SyncableExcalidrawElement = ExcalidrawElement & { - _brand: "SyncableExcalidrawElement"; -}; +export type SyncableExcalidrawElement = OrderedExcalidrawElement & + MakeBrand<"SyncableExcalidrawElement">; export const isSyncableElement = ( - element: ExcalidrawElement, + element: OrderedExcalidrawElement, ): element is SyncableExcalidrawElement => { if (element.isDeleted) { if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) { @@ -51,7 +52,9 @@ export const isSyncableElement = ( return !isInvisiblySmallElement(element); }; -export const getSyncableElements = (elements: readonly ExcalidrawElement[]) => +export const getSyncableElements = ( + elements: readonly OrderedExcalidrawElement[], +) => elements.filter((element) => isSyncableElement(element), ) as SyncableExcalidrawElement[]; diff --git a/excalidraw-app/tests/collab.test.tsx b/excalidraw-app/tests/collab.test.tsx index c3e94a5ef414c..2e2f1332a1c3b 100644 --- a/excalidraw-app/tests/collab.test.tsx +++ b/excalidraw-app/tests/collab.test.tsx @@ -7,6 +7,8 @@ import { import ExcalidrawApp from "../App"; import { API } from "../../packages/excalidraw/tests/helpers/api"; import { createUndoAction } from "../../packages/excalidraw/actions/actionHistory"; +import { syncInvalidIndices } from "../../packages/excalidraw/fractionalIndex"; + const { h } = window; Object.defineProperty(window, "crypto", { @@ -61,14 +63,14 @@ describe("collaboration", () => { await render(); // To update the scene with deleted elements before starting collab updateSceneData({ - elements: [ + elements: syncInvalidIndices([ API.createElement({ type: "rectangle", id: "A" }), API.createElement({ type: "rectangle", id: "B", isDeleted: true, }), - ], + ]), }); await waitFor(() => { expect(h.elements).toEqual([ diff --git a/excalidraw-app/tests/reconciliation.test.ts b/excalidraw-app/tests/reconciliation.test.ts deleted file mode 100644 index 8e395474c6745..0000000000000 --- a/excalidraw-app/tests/reconciliation.test.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { expect } from "chai"; -import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants"; -import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; -import { - BroadcastedExcalidrawElement, - ReconciledElements, - reconcileElements, -} from "../../excalidraw-app/collab/reconciliation"; -import { randomInteger } from "../../packages/excalidraw/random"; -import { AppState } from "../../packages/excalidraw/types"; -import { cloneJSON } from "../../packages/excalidraw/utils"; - -type Id = string; -type ElementLike = { - id: string; - version: number; - versionNonce: number; - [PRECEDING_ELEMENT_KEY]?: string | null; -}; - -type Cache = Record; - -const createElement = (opts: { uid: string } | ElementLike) => { - let uid: string; - let id: string; - let version: number | null; - let parent: string | null = null; - let versionNonce: number | null = null; - if ("uid" in opts) { - const match = opts.uid.match( - /^(?:\((\^|\w+)\))?(\w+)(?::(\d+))?(?:\((\w+)\))?$/, - )!; - parent = match[1]; - id = match[2]; - version = match[3] ? parseInt(match[3]) : null; - uid = version ? `${id}:${version}` : id; - } else { - ({ id, version, versionNonce } = opts); - parent = parent || null; - uid = id; - } - return { - uid, - id, - version, - versionNonce: versionNonce || randomInteger(), - [PRECEDING_ELEMENT_KEY]: parent || null, - }; -}; - -const idsToElements = ( - ids: (Id | ElementLike)[], - cache: Cache = {}, -): readonly ExcalidrawElement[] => { - return ids.reduce((acc, _uid, idx) => { - const { - uid, - id, - version, - [PRECEDING_ELEMENT_KEY]: parent, - versionNonce, - } = createElement(typeof _uid === "string" ? { uid: _uid } : _uid); - const cached = cache[uid]; - const elem = { - id, - version: version ?? 0, - versionNonce, - ...cached, - [PRECEDING_ELEMENT_KEY]: parent, - } as BroadcastedExcalidrawElement; - // @ts-ignore - cache[uid] = elem; - acc.push(elem); - return acc; - }, [] as ExcalidrawElement[]); -}; - -const addParents = (elements: BroadcastedExcalidrawElement[]) => { - return elements.map((el, idx, els) => { - el[PRECEDING_ELEMENT_KEY] = els[idx - 1]?.id || "^"; - return el; - }); -}; - -const cleanElements = (elements: ReconciledElements) => { - return elements.map((el) => { - // @ts-ignore - delete el[PRECEDING_ELEMENT_KEY]; - // @ts-ignore - delete el.next; - // @ts-ignore - delete el.prev; - return el; - }); -}; - -const test = ( - local: (Id | ElementLike)[], - remote: (Id | ElementLike)[], - target: U[], - bidirectional = true, -) => { - const cache: Cache = {}; - const _local = idsToElements(local, cache); - const _remote = idsToElements(remote, cache); - const _target = target.map((uid) => { - const [, id, source] = uid.match(/^(\w+):([LR])$/)!; - return (source === "L" ? _local : _remote).find((e) => e.id === id)!; - }) as any as ReconciledElements; - const remoteReconciled = reconcileElements(_local, _remote, {} as AppState); - expect(target.length).equal(remoteReconciled.length); - expect(cleanElements(remoteReconciled)).deep.equal( - cleanElements(_target), - "remote reconciliation", - ); - - const __local = cleanElements(cloneJSON(_remote) as ReconciledElements); - const __remote = addParents(cleanElements(cloneJSON(remoteReconciled))); - if (bidirectional) { - try { - expect( - cleanElements( - reconcileElements( - cloneJSON(__local), - cloneJSON(__remote), - {} as AppState, - ), - ), - ).deep.equal(cleanElements(remoteReconciled), "local re-reconciliation"); - } catch (error: any) { - console.error("local original", __local); - console.error("remote reconciled", __remote); - throw error; - } - } -}; - -export const findIndex = ( - array: readonly T[], - cb: (element: T, index: number, array: readonly T[]) => boolean, - fromIndex: number = 0, -) => { - if (fromIndex < 0) { - fromIndex = array.length + fromIndex; - } - fromIndex = Math.min(array.length, Math.max(fromIndex, 0)); - let index = fromIndex - 1; - while (++index < array.length) { - if (cb(array[index], index, array)) { - return index; - } - } - return -1; -}; - -// ----------------------------------------------------------------------------- - -describe("elements reconciliation", () => { - it("reconcileElements()", () => { - // ------------------------------------------------------------------------- - // - // in following tests, we pass: - // (1) an array of local elements and their version (:1, :2...) - // (2) an array of remote elements and their version (:1, :2...) - // (3) expected reconciled elements - // - // in the reconciled array: - // :L means local element was resolved - // :R means remote element was resolved - // - // if a remote element is prefixed with parentheses, the enclosed string: - // (^) means the element is the first element in the array - // () means the element is preceded by element - // - // if versions are missing, it defaults to version 0 - // ------------------------------------------------------------------------- - - // non-annotated elements - // ------------------------------------------------------------------------- - // usually when we sync elements they should always be annotated with - // their (preceding elements) parents, but let's test a couple of cases when - // they're not for whatever reason (remote clients are on older version...), - // in which case the first synced element either replaces existing element - // or is pushed at the end of the array - - test(["A:1", "B:1", "C:1"], ["B:2"], ["A:L", "B:R", "C:L"]); - test(["A:1", "B:1", "C"], ["B:2", "A:2"], ["B:R", "A:R", "C:L"]); - test(["A:2", "B:1", "C"], ["B:2", "A:1"], ["A:L", "B:R", "C:L"]); - test(["A:1", "B:1"], ["C:1"], ["A:L", "B:L", "C:R"]); - test(["A", "B"], ["A:1"], ["A:R", "B:L"]); - test(["A"], ["A", "B"], ["A:L", "B:R"]); - test(["A"], ["A:1", "B"], ["A:R", "B:R"]); - test(["A:2"], ["A:1", "B"], ["A:L", "B:R"]); - test(["A:2"], ["B", "A:1"], ["A:L", "B:R"]); - test(["A:1"], ["B", "A:2"], ["B:R", "A:R"]); - test(["A"], ["A:1"], ["A:R"]); - - // C isn't added to the end because it follows B (even if B was resolved - // to local version) - test(["A", "B:1", "D"], ["B", "C:2", "A"], ["B:L", "C:R", "A:R", "D:L"]); - - // some of the following tests are kinda arbitrary and they're less - // likely to happen in real-world cases - - test(["A", "B"], ["B:1", "A:1"], ["B:R", "A:R"]); - test(["A:2", "B:2"], ["B:1", "A:1"], ["A:L", "B:L"]); - test(["A", "B", "C"], ["A", "B:2", "G", "C"], ["A:L", "B:R", "G:R", "C:L"]); - test(["A", "B", "C"], ["A", "B:2", "G"], ["A:L", "B:R", "G:R", "C:L"]); - test(["A", "B", "C"], ["A", "B:2", "G"], ["A:L", "B:R", "G:R", "C:L"]); - test( - ["A:2", "B:2", "C"], - ["D", "B:1", "A:3"], - ["B:L", "A:R", "C:L", "D:R"], - ); - test( - ["A:2", "B:2", "C"], - ["D", "B:2", "A:3", "C"], - ["D:R", "B:L", "A:R", "C:L"], - ); - test( - ["A", "B", "C", "D", "E", "F"], - ["A", "B:2", "X", "E:2", "F", "Y"], - ["A:L", "B:R", "X:R", "E:R", "F:L", "Y:R", "C:L", "D:L"], - ); - - // annotated elements - // ------------------------------------------------------------------------- - - test( - ["A", "B", "C"], - ["(B)X", "(A)Y", "(Y)Z"], - ["A:L", "B:L", "X:R", "Y:R", "Z:R", "C:L"], - ); - - test(["A"], ["(^)X", "Y"], ["X:R", "Y:R", "A:L"]); - test(["A"], ["(^)X", "Y", "Z"], ["X:R", "Y:R", "Z:R", "A:L"]); - - test( - ["A", "B"], - ["(A)C", "(^)D", "F"], - ["A:L", "C:R", "D:R", "F:R", "B:L"], - ); - - test( - ["A", "B", "C", "D"], - ["(B)C:1", "B", "D:1"], - ["A:L", "C:R", "B:L", "D:R"], - ); - - test( - ["A", "B", "C"], - ["(^)X", "(A)Y", "(B)Z"], - ["X:R", "A:L", "Y:R", "B:L", "Z:R", "C:L"], - ); - - test( - ["B", "A", "C"], - ["(^)X", "(A)Y", "(B)Z"], - ["X:R", "B:L", "A:L", "Y:R", "Z:R", "C:L"], - ); - - test(["A", "B"], ["(A)X", "(A)Y"], ["A:L", "X:R", "Y:R", "B:L"]); - - test( - ["A", "B", "C", "D", "E"], - ["(A)X", "(C)Y", "(D)Z"], - ["A:L", "X:R", "B:L", "C:L", "Y:R", "D:L", "Z:R", "E:L"], - ); - - test( - ["X", "Y", "Z"], - ["(^)A", "(A)B", "(B)C", "(C)X", "(X)D", "(D)Y", "(Y)Z"], - ["A:R", "B:R", "C:R", "X:L", "D:R", "Y:L", "Z:L"], - ); - - test( - ["A", "B", "C", "D", "E"], - ["(C)X", "(A)Y", "(D)E:1"], - ["A:L", "B:L", "C:L", "X:R", "Y:R", "D:L", "E:R"], - ); - - test( - ["C:1", "B", "D:1"], - ["A", "B", "C:1", "D:1"], - ["A:R", "B:L", "C:L", "D:L"], - ); - - test( - ["A", "B", "C", "D"], - ["(A)C:1", "(C)B", "(B)D:1"], - ["A:L", "C:R", "B:L", "D:R"], - ); - - test( - ["A", "B", "C", "D"], - ["(A)C:1", "(C)B", "(B)D:1"], - ["A:L", "C:R", "B:L", "D:R"], - ); - - test( - ["C:1", "B", "D:1"], - ["(^)A", "(A)B", "(B)C:2", "(C)D:1"], - ["A:R", "B:L", "C:R", "D:L"], - ); - - test( - ["A", "B", "C", "D"], - ["(C)X", "(B)Y", "(A)Z"], - ["A:L", "B:L", "C:L", "X:R", "Y:R", "Z:R", "D:L"], - ); - - test(["A", "B", "C", "D"], ["(A)B:1", "C:1"], ["A:L", "B:R", "C:R", "D:L"]); - test(["A", "B", "C", "D"], ["(A)C:1", "B:1"], ["A:L", "C:R", "B:R", "D:L"]); - test( - ["A", "B", "C", "D"], - ["(A)C:1", "B", "D:1"], - ["A:L", "C:R", "B:L", "D:R"], - ); - - test(["A:1", "B:1", "C"], ["B:2"], ["A:L", "B:R", "C:L"]); - test(["A:1", "B:1", "C"], ["B:2", "C:2"], ["A:L", "B:R", "C:R"]); - - test(["A", "B"], ["(A)C", "(B)D"], ["A:L", "C:R", "B:L", "D:R"]); - test(["A", "B"], ["(X)C", "(X)D"], ["A:L", "B:L", "C:R", "D:R"]); - test(["A", "B"], ["(X)C", "(A)D"], ["A:L", "D:R", "B:L", "C:R"]); - test(["A", "B"], ["(A)B:1"], ["A:L", "B:R"]); - test(["A:2", "B"], ["(A)B:1"], ["A:L", "B:R"]); - test(["A:2", "B:2"], ["B:1"], ["A:L", "B:L"]); - test(["A:2", "B:2"], ["B:1", "C"], ["A:L", "B:L", "C:R"]); - test(["A:2", "B:2"], ["(A)C", "B:1"], ["A:L", "C:R", "B:L"]); - test(["A:2", "B:2"], ["(A)C", "B:1"], ["A:L", "C:R", "B:L"]); - }); - - it("test identical elements reconciliation", () => { - const testIdentical = ( - local: ElementLike[], - remote: ElementLike[], - expected: Id[], - ) => { - const ret = reconcileElements( - local as any as ExcalidrawElement[], - remote as any as ExcalidrawElement[], - {} as AppState, - ); - - if (new Set(ret.map((x) => x.id)).size !== ret.length) { - throw new Error("reconcileElements: duplicate elements found"); - } - - expect(ret.map((x) => x.id)).to.deep.equal(expected); - }; - - // identical id/version/versionNonce - // ------------------------------------------------------------------------- - - testIdentical( - [{ id: "A", version: 1, versionNonce: 1 }], - [{ id: "A", version: 1, versionNonce: 1 }], - ["A"], - ); - testIdentical( - [ - { id: "A", version: 1, versionNonce: 1 }, - { id: "B", version: 1, versionNonce: 1 }, - ], - [ - { id: "B", version: 1, versionNonce: 1 }, - { id: "A", version: 1, versionNonce: 1 }, - ], - ["B", "A"], - ); - testIdentical( - [ - { id: "A", version: 1, versionNonce: 1 }, - { id: "B", version: 1, versionNonce: 1 }, - ], - [ - { id: "B", version: 1, versionNonce: 1 }, - { id: "A", version: 1, versionNonce: 1 }, - ], - ["B", "A"], - ); - - // actually identical (arrays and element objects) - // ------------------------------------------------------------------------- - - const elements1 = [ - { - id: "A", - version: 1, - versionNonce: 1, - [PRECEDING_ELEMENT_KEY]: null, - }, - { - id: "B", - version: 1, - versionNonce: 1, - [PRECEDING_ELEMENT_KEY]: null, - }, - ]; - - testIdentical(elements1, elements1, ["A", "B"]); - testIdentical(elements1, elements1.slice(), ["A", "B"]); - testIdentical(elements1.slice(), elements1, ["A", "B"]); - testIdentical(elements1.slice(), elements1.slice(), ["A", "B"]); - - const el1 = { - id: "A", - version: 1, - versionNonce: 1, - [PRECEDING_ELEMENT_KEY]: null, - }; - const el2 = { - id: "B", - version: 1, - versionNonce: 1, - [PRECEDING_ELEMENT_KEY]: null, - }; - testIdentical([el1, el2], [el2, el1], ["A", "B"]); - }); -}); diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index daefa569155be..9e3a31ad4efb1 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -31,8 +31,9 @@ import { } from "../element/types"; import { AppState } from "../types"; import { Mutable } from "../utility-types"; -import { getFontString } from "../utils"; +import { arrayToMap, getFontString } from "../utils"; import { register } from "./register"; +import { syncMovedIndices } from "../fractionalIndex"; export const actionUnbindText = register({ name: "unbindText", @@ -180,6 +181,8 @@ const pushTextAboveContainer = ( (ele) => ele.id === container.id, ); updatedElements.splice(containerIndex + 1, 0, textElement); + syncMovedIndices(updatedElements, arrayToMap([container, textElement])); + return updatedElements; }; @@ -198,6 +201,8 @@ const pushContainerBelowText = ( (ele) => ele.id === textElement.id, ); updatedElements.splice(textElementIndex, 0, container); + syncMovedIndices(updatedElements, arrayToMap([container, textElement])); + return updatedElements; }; @@ -304,6 +309,7 @@ export const actionWrapTextInContainer = register({ container, textElement, ); + containerIds[container.id] = true; } } diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index 86391f9e327f3..73d5717c390a2 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -31,6 +31,7 @@ import { excludeElementsInFramesFromSelection, getSelectedElements, } from "../scene/selection"; +import { syncMovedIndices } from "../fractionalIndex"; export const actionDuplicateSelection = register({ name: "duplicateSelection", @@ -89,6 +90,7 @@ const duplicateElements = ( const newElements: ExcalidrawElement[] = []; const oldElements: ExcalidrawElement[] = []; const oldIdToDuplicatedId = new Map(); + const duplicatedElementsMap = new Map(); const duplicateAndOffsetElement = (element: ExcalidrawElement) => { const newElement = duplicateElement( @@ -100,6 +102,7 @@ const duplicateElements = ( y: element.y + GRID_SIZE / 2, }, ); + duplicatedElementsMap.set(newElement.id, newElement); oldIdToDuplicatedId.set(element.id, newElement.id); oldElements.push(element); newElements.push(newElement); @@ -237,9 +240,10 @@ const duplicateElements = ( } // step (3) - const finalElements = finalElementsReversed.reverse(); + syncMovedIndices(finalElements, arrayToMap([...oldElements, ...newElements])); + // --------------------------------------------------------------------------- bindTextToShapeAfterDuplication( diff --git a/packages/excalidraw/actions/actionGroup.tsx b/packages/excalidraw/actions/actionGroup.tsx index 44e590bc26e4d..0643ca438857d 100644 --- a/packages/excalidraw/actions/actionGroup.tsx +++ b/packages/excalidraw/actions/actionGroup.tsx @@ -27,6 +27,7 @@ import { removeElementsFromFrame, replaceAllElementsInFrame, } from "../frame"; +import { syncMovedIndices } from "../fractionalIndex"; const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => { if (elements.length >= 2) { @@ -138,11 +139,12 @@ export const actionGroup = register({ .filter( (updatedElement) => !isElementInGroup(updatedElement, newGroupId), ); - nextElements = [ + const reorderedElements = [ ...elementsBeforeGroup, ...elementsInGroup, ...elementsAfterGroup, ]; + syncMovedIndices(reorderedElements, arrayToMap(elementsInGroup)); return { appState: { @@ -153,7 +155,7 @@ export const actionGroup = register({ getNonDeletedElements(nextElements), ), }, - elements: nextElements, + elements: reorderedElements, commitToHistory: true, }; }, diff --git a/packages/excalidraw/actions/actionHistory.tsx b/packages/excalidraw/actions/actionHistory.tsx index 2e0f4c0916eeb..bf0a87cf548fc 100644 --- a/packages/excalidraw/actions/actionHistory.tsx +++ b/packages/excalidraw/actions/actionHistory.tsx @@ -10,6 +10,7 @@ import { newElementWith } from "../element/mutateElement"; import { fixBindingsAfterDeletion } from "../element/binding"; import { arrayToMap } from "../utils"; import { isWindows } from "../constants"; +import { syncInvalidIndices } from "../fractionalIndex"; const writeData = ( prevElements: readonly ExcalidrawElement[], @@ -48,6 +49,8 @@ const writeData = ( ), ); fixBindingsAfterDeletion(elements, deletedElements); + // TODO: will be replaced in #7348 + syncInvalidIndices(elements); return { elements, diff --git a/packages/excalidraw/actions/actionZindex.tsx b/packages/excalidraw/actions/actionZindex.tsx index 17ecde1a63527..0ecfdcb2e651f 100644 --- a/packages/excalidraw/actions/actionZindex.tsx +++ b/packages/excalidraw/actions/actionZindex.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { moveOneLeft, moveOneRight, diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 586e69e37c477..1fe00fc9fec8e 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -182,6 +182,7 @@ import { IframeData, ExcalidrawIframeElement, ExcalidrawEmbeddableElement, + Ordered, } from "../element/types"; import { getCenter, getDistance } from "../gesture"; import { @@ -269,6 +270,7 @@ import { muteFSAbortError, isTestEnv, easeOut, + arrayToMap, updateStable, addEventListener, normalizeEOL, @@ -402,7 +404,6 @@ import { ElementCanvasButton } from "./MagicButton"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; import { EditorLocalStorage } from "../data/EditorLocalStorage"; import FollowMode from "./FollowMode/FollowMode"; - import { AnimationFrameHandler } from "../animation-frame-handler"; import { AnimatedTrail } from "../animated-trail"; import { LaserTrails } from "../laser-trails"; @@ -410,6 +411,7 @@ import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; import { getRenderOpacity } from "../renderer/renderElement"; import { textWysiwyg } from "../element/textWysiwyg"; import { isOverScrollBars } from "../scene/scrollbars"; +import { syncInvalidIndices, syncMovedIndices } from "../fractionalIndex"; import { isPointHittingLink, isPointHittingLinkIcon, @@ -936,7 +938,7 @@ class App extends React.Component { const embeddableElements = this.scene .getNonDeletedElements() .filter( - (el): el is NonDeleted => + (el): el is Ordered> => (isEmbeddableElement(el) && this.embedsValidationStatus.get(el.id) === true) || isIframeElement(el), @@ -2057,7 +2059,7 @@ class App extends React.Component { locked: false, }); - this.scene.addNewElement(frame); + this.scene.insertElement(frame); for (const child of selectedElements) { mutateElement(child, { frameId: frame.id }); @@ -3176,10 +3178,10 @@ class App extends React.Component { }, ); - const allElements = [ - ...this.scene.getElementsIncludingDeleted(), - ...newElements, - ]; + const prevElements = this.scene.getElementsIncludingDeleted(); + const nextElements = [...prevElements, ...newElements]; + + syncMovedIndices(nextElements, arrayToMap(newElements)); const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y }); @@ -3188,10 +3190,10 @@ class App extends React.Component { newElements, topLayerFrame, ); - addElementsToFrame(allElements, eligibleElements, topLayerFrame); + addElementsToFrame(nextElements, eligibleElements, topLayerFrame); } - this.scene.replaceAllElements(allElements); + this.scene.replaceAllElements(nextElements); newElements.forEach((newElement) => { if (isTextElement(newElement) && isBoundToContainer(newElement)) { @@ -3422,19 +3424,7 @@ class App extends React.Component { return; } - const frameId = textElements[0].frameId; - - if (frameId) { - this.scene.insertElementsAtIndex( - textElements, - this.scene.getElementIndex(frameId), - ); - } else { - this.scene.replaceAllElements([ - ...this.scene.getElementsIncludingDeleted(), - ...textElements, - ]); - } + this.scene.insertElements(textElements); this.setState({ selectedElementIds: makeNextSelectedElementIds( @@ -4647,7 +4637,7 @@ class App extends React.Component { const containerIndex = this.scene.getElementIndex(container.id); this.scene.insertElementAtIndex(element, containerIndex + 1); } else { - this.scene.addNewElement(element); + this.scene.insertElement(element); } } @@ -6580,7 +6570,7 @@ class App extends React.Component { this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), ); - this.scene.addNewElement(element); + this.scene.insertElement(element); this.setState({ draggingElement: element, editingElement: element, @@ -6625,10 +6615,7 @@ class App extends React.Component { height, }); - this.scene.replaceAllElements([ - ...this.scene.getElementsIncludingDeleted(), - element, - ]); + this.scene.insertElement(element); return element; }; @@ -6682,10 +6669,7 @@ class App extends React.Component { link, }); - this.scene.replaceAllElements([ - ...this.scene.getElementsIncludingDeleted(), - element, - ]); + this.scene.insertElement(element); return element; }; @@ -6850,7 +6834,7 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), ); - this.scene.addNewElement(element); + this.scene.insertElement(element); this.setState({ draggingElement: element, editingElement: element, @@ -6929,7 +6913,7 @@ class App extends React.Component { draggingElement: element, }); } else { - this.scene.addNewElement(element); + this.scene.insertElement(element); this.setState({ multiElement: null, draggingElement: element, @@ -6963,10 +6947,7 @@ class App extends React.Component { ? newMagicFrameElement(constructorOpts) : newFrameElement(constructorOpts); - this.scene.replaceAllElements([ - ...this.scene.getElementsIncludingDeleted(), - frame, - ]); + this.scene.insertElement(frame); this.setState({ multiElement: null, @@ -7379,7 +7360,11 @@ class App extends React.Component { nextElements.push(element); } } + const nextSceneElements = [...nextElements, ...elementsToAppend]; + + syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend)); + bindTextToShapeAfterDuplication( nextElements, elementsToAppend, @@ -7396,6 +7381,7 @@ class App extends React.Component { elementsToAppend, oldIdToDuplicatedId, ); + this.scene.replaceAllElements(nextSceneElements); this.maybeCacheVisibleGaps(event, selectedElements, true); this.maybeCacheReferenceSnapPoints(event, selectedElements, true); @@ -8573,7 +8559,7 @@ class App extends React.Component { return; } - this.scene.addNewElement(imageElement); + this.scene.insertElement(imageElement); try { return await this.initializeImage({ @@ -9744,7 +9730,9 @@ export const createTestHook = () => { return this.app?.scene.getElementsIncludingDeleted(); }, set(elements: ExcalidrawElement[]) { - return this.app?.scene.replaceAllElements(elements); + return this.app?.scene.replaceAllElements( + syncInvalidIndices(elements), + ); }, }, }); diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts index 2d4e57c6e49e8..03615aeb842c2 100644 --- a/packages/excalidraw/constants.ts +++ b/packages/excalidraw/constants.ts @@ -320,10 +320,6 @@ export const ROUNDNESS = { ADAPTIVE_RADIUS: 3, } as const; -/** key containt id of precedeing elemnt id we use in reconciliation during - * collaboration */ -export const PRECEDING_ELEMENT_KEY = "__precedingElement__"; - export const ROUGHNESS = { architect: 0, artist: 1, diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap index 2ae7eced88428..6baac1d9dec8a 100644 --- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -20,6 +20,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "groupIds": [], "height": 300, "id": Any, + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -32,7 +33,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 3, + "version": 4, "versionNonce": Any, "width": 300, "x": 630, @@ -56,6 +57,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "groupIds": [], "height": 100, "id": Any, + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -68,7 +70,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 140, "x": 96, @@ -93,6 +95,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "groupIds": [], "height": 35, "id": Any, + "index": "a2", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -122,7 +125,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, + "version": 4, "versionNonce": Any, "width": 395, "x": 247, @@ -147,6 +150,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "groupIds": [], "height": 0, "id": Any, + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -176,7 +180,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, + "version": 4, "versionNonce": Any, "width": 400, "x": 227, @@ -200,6 +204,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "groupIds": [], "height": 300, "id": Any, + "index": "a4", "isDeleted": false, "link": null, "locked": false, @@ -212,7 +217,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 300, "x": -53, @@ -239,6 +244,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "groupIds": [], "height": 25, "id": Any, + "index": "a0", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -255,7 +261,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "textAlign": "left", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "top", "width": 70, @@ -283,6 +289,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "groupIds": [], "height": 25, "id": Any, + "index": "a1", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -299,7 +306,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "textAlign": "left", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "top", "width": 100, @@ -330,6 +337,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "groupIds": [], "height": 0, "id": Any, + "index": "a2", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -359,7 +367,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, + "version": 4, "versionNonce": Any, "width": 100, "x": 255, @@ -381,6 +389,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "groupIds": [], "height": 25, "id": Any, + "index": "a3", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -397,7 +406,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 130, @@ -428,6 +437,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "groupIds": [], "height": 0, "id": Any, + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -457,7 +467,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, + "version": 4, "versionNonce": Any, "width": 100, "x": 255, @@ -479,6 +489,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "groupIds": [], "height": 25, "id": Any, + "index": "a1", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -495,7 +506,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 130, @@ -520,6 +531,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "groupIds": [], "height": 100, "id": Any, + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -532,7 +544,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 100, "x": 155, @@ -556,6 +568,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "groupIds": [], "height": 100, "id": Any, + "index": "a3", "isDeleted": false, "link": null, "locked": false, @@ -568,7 +581,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 100, "x": 355, @@ -598,6 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "groupIds": [], "height": 0, "id": Any, + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -627,7 +641,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, + "version": 4, "versionNonce": Any, "width": 100, "x": 255, @@ -649,6 +663,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "groupIds": [], "height": 25, "id": Any, + "index": "a1", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -665,7 +680,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 130, @@ -693,6 +708,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "groupIds": [], "height": 25, "id": Any, + "index": "a2", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -709,7 +725,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "textAlign": "left", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "top", "width": 70, @@ -737,6 +753,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "groupIds": [], "height": 25, "id": Any, + "index": "a3", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -753,7 +770,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "textAlign": "left", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "top", "width": 100, @@ -773,6 +790,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = ` "groupIds": [], "height": 200, "id": "rect-1", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -785,7 +803,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 300, @@ -806,6 +824,7 @@ exports[`Test Transform > should transform linear elements 1`] = ` "groupIds": [], "height": 0, "id": Any, + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -831,7 +850,7 @@ exports[`Test Transform > should transform linear elements 1`] = ` "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -852,6 +871,7 @@ exports[`Test Transform > should transform linear elements 2`] = ` "groupIds": [], "height": 0, "id": Any, + "index": "a1", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -877,7 +897,7 @@ exports[`Test Transform > should transform linear elements 2`] = ` "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 450, @@ -898,6 +918,7 @@ exports[`Test Transform > should transform linear elements 3`] = ` "groupIds": [], "height": 0, "id": Any, + "index": "a2", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -923,7 +944,7 @@ exports[`Test Transform > should transform linear elements 3`] = ` "strokeWidth": 2, "type": "line", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -944,6 +965,7 @@ exports[`Test Transform > should transform linear elements 4`] = ` "groupIds": [], "height": 0, "id": Any, + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -969,7 +991,7 @@ exports[`Test Transform > should transform linear elements 4`] = ` "strokeWidth": 2, "type": "line", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 450, @@ -988,6 +1010,7 @@ exports[`Test Transform > should transform regular shapes 1`] = ` "groupIds": [], "height": 100, "id": Any, + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1000,7 +1023,7 @@ exports[`Test Transform > should transform regular shapes 1`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -1019,6 +1042,7 @@ exports[`Test Transform > should transform regular shapes 2`] = ` "groupIds": [], "height": 100, "id": Any, + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1031,7 +1055,7 @@ exports[`Test Transform > should transform regular shapes 2`] = ` "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -1050,6 +1074,7 @@ exports[`Test Transform > should transform regular shapes 3`] = ` "groupIds": [], "height": 100, "id": Any, + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1062,7 +1087,7 @@ exports[`Test Transform > should transform regular shapes 3`] = ` "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -1081,6 +1106,7 @@ exports[`Test Transform > should transform regular shapes 4`] = ` "groupIds": [], "height": 100, "id": Any, + "index": "a3", "isDeleted": false, "link": null, "locked": false, @@ -1093,7 +1119,7 @@ exports[`Test Transform > should transform regular shapes 4`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 200, "x": 300, @@ -1112,6 +1138,7 @@ exports[`Test Transform > should transform regular shapes 5`] = ` "groupIds": [], "height": 100, "id": Any, + "index": "a4", "isDeleted": false, "link": null, "locked": false, @@ -1124,7 +1151,7 @@ exports[`Test Transform > should transform regular shapes 5`] = ` "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 200, "x": 300, @@ -1143,6 +1170,7 @@ exports[`Test Transform > should transform regular shapes 6`] = ` "groupIds": [], "height": 100, "id": Any, + "index": "a5", "isDeleted": false, "link": null, "locked": false, @@ -1155,7 +1183,7 @@ exports[`Test Transform > should transform regular shapes 6`] = ` "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 200, "x": 300, @@ -1177,6 +1205,7 @@ exports[`Test Transform > should transform text element 1`] = ` "groupIds": [], "height": 25, "id": Any, + "index": "a0", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1193,7 +1222,7 @@ exports[`Test Transform > should transform text element 1`] = ` "textAlign": "left", "type": "text", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "verticalAlign": "top", "width": 120, @@ -1216,6 +1245,7 @@ exports[`Test Transform > should transform text element 2`] = ` "groupIds": [], "height": 25, "id": Any, + "index": "a1", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1232,7 +1262,7 @@ exports[`Test Transform > should transform text element 2`] = ` "textAlign": "left", "type": "text", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "verticalAlign": "top", "width": 190, @@ -1259,6 +1289,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 0, "id": Any, + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -1284,7 +1315,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -1310,6 +1341,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 0, "id": Any, + "index": "a1", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -1335,7 +1367,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -1361,6 +1393,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 0, "id": Any, + "index": "a2", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -1386,7 +1419,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -1412,6 +1445,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 0, "id": Any, + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -1437,7 +1471,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 1, + "version": 2, "versionNonce": Any, "width": 100, "x": 100, @@ -1459,6 +1493,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 25, "id": Any, + "index": "a4", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1475,7 +1510,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 130, @@ -1498,6 +1533,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 25, "id": Any, + "index": "a5", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1514,7 +1550,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 200, @@ -1537,6 +1573,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 50, "id": Any, + "index": "a6", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1554,7 +1591,7 @@ LABELLED ARROW", "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 150, @@ -1577,6 +1614,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "groupIds": [], "height": 50, "id": Any, + "index": "a7", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1594,7 +1632,7 @@ LABELLED ARROW", "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 150, @@ -1619,6 +1657,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 35, "id": Any, + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1631,7 +1670,7 @@ exports[`Test Transform > should transform to text containers when label provide "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, + "version": 4, "versionNonce": Any, "width": 250, "x": 100, @@ -1655,6 +1694,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 85, "id": Any, + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1667,7 +1707,7 @@ exports[`Test Transform > should transform to text containers when label provide "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 200, "x": 500, @@ -1691,6 +1731,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 170, "id": Any, + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1703,7 +1744,7 @@ exports[`Test Transform > should transform to text containers when label provide "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 280, "x": 100, @@ -1727,6 +1768,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 120, "id": Any, + "index": "a3", "isDeleted": false, "link": null, "locked": false, @@ -1739,7 +1781,7 @@ exports[`Test Transform > should transform to text containers when label provide "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 300, "x": 100, @@ -1763,6 +1805,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 85, "id": Any, + "index": "a4", "isDeleted": false, "link": null, "locked": false, @@ -1775,7 +1818,7 @@ exports[`Test Transform > should transform to text containers when label provide "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 200, "x": 500, @@ -1799,6 +1842,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 120, "id": Any, + "index": "a5", "isDeleted": false, "link": null, "locked": false, @@ -1811,7 +1855,7 @@ exports[`Test Transform > should transform to text containers when label provide "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "width": 200, "x": 500, @@ -1833,6 +1877,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 25, "id": Any, + "index": "a6", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1849,7 +1894,7 @@ exports[`Test Transform > should transform to text containers when label provide "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 240, @@ -1872,6 +1917,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 50, "id": Any, + "index": "a7", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1889,7 +1935,7 @@ CONTAINER", "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 130, @@ -1912,6 +1958,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 75, "id": Any, + "index": "a8", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1931,7 +1978,7 @@ CONTAINER", "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 90, @@ -1954,6 +2001,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 50, "id": Any, + "index": "a9", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -1971,7 +2019,7 @@ TEXT CONTAINER", "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 140, @@ -1994,6 +2042,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 75, "id": Any, + "index": "aA", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -2012,7 +2061,7 @@ CONTAINER", "textAlign": "left", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "top", "width": 170, @@ -2035,6 +2084,7 @@ exports[`Test Transform > should transform to text containers when label provide "groupIds": [], "height": 75, "id": Any, + "index": "aB", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -2053,7 +2103,7 @@ CONTAINER", "textAlign": "center", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "middle", "width": 130, diff --git a/packages/excalidraw/data/reconcile.ts b/packages/excalidraw/data/reconcile.ts new file mode 100644 index 0000000000000..95d6e4669c15c --- /dev/null +++ b/packages/excalidraw/data/reconcile.ts @@ -0,0 +1,79 @@ +import { OrderedExcalidrawElement } from "../element/types"; +import { orderByFractionalIndex, syncInvalidIndices } from "../fractionalIndex"; +import { AppState } from "../types"; +import { MakeBrand } from "../utility-types"; +import { arrayToMap } from "../utils"; + +export type ReconciledExcalidrawElement = OrderedExcalidrawElement & + MakeBrand<"ReconciledElement">; + +export type RemoteExcalidrawElement = OrderedExcalidrawElement & + MakeBrand<"RemoteExcalidrawElement">; + +const shouldDiscardRemoteElement = ( + localAppState: AppState, + local: OrderedExcalidrawElement | undefined, + remote: RemoteExcalidrawElement, +): boolean => { + if ( + local && + // local element is being edited + (local.id === localAppState.editingElement?.id || + local.id === localAppState.resizingElement?.id || + local.id === localAppState.draggingElement?.id || + // local element is newer + local.version > remote.version || + // resolve conflicting edits deterministically by taking the one with + // the lowest versionNonce + (local.version === remote.version && + local.versionNonce < remote.versionNonce)) + ) { + return true; + } + return false; +}; + +export const reconcileElements = ( + localElements: readonly OrderedExcalidrawElement[], + remoteElements: readonly RemoteExcalidrawElement[], + localAppState: AppState, +): ReconciledExcalidrawElement[] => { + const localElementsMap = arrayToMap(localElements); + const reconciledElements: OrderedExcalidrawElement[] = []; + const added = new Set(); + + // process remote elements + for (const remoteElement of remoteElements) { + if (!added.has(remoteElement.id)) { + const localElement = localElementsMap.get(remoteElement.id); + const discardRemoteElement = shouldDiscardRemoteElement( + localAppState, + localElement, + remoteElement, + ); + + if (localElement && discardRemoteElement) { + reconciledElements.push(localElement); + added.add(localElement.id); + } else { + reconciledElements.push(remoteElement); + added.add(remoteElement.id); + } + } + } + + // process remaining local elements + for (const localElement of localElements) { + if (!added.has(localElement.id)) { + reconciledElements.push(localElement); + added.add(localElement.id); + } + } + + const orderedElements = orderByFractionalIndex(reconciledElements); + + // de-duplicate indices + syncInvalidIndices(orderedElements); + + return orderedElements as ReconciledExcalidrawElement[]; +}; diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 0ff0203dc28d8..d8cadeac8e6e0 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -4,6 +4,7 @@ import { ExcalidrawSelectionElement, ExcalidrawTextElement, FontFamilyValues, + OrderedExcalidrawElement, PointBinding, StrokeRoundness, } from "../element/types"; @@ -26,7 +27,6 @@ import { DEFAULT_FONT_FAMILY, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, - PRECEDING_ELEMENT_KEY, FONT_FAMILY, ROUNDNESS, DEFAULT_SIDEBAR, @@ -44,6 +44,7 @@ import { getDefaultLineHeight, } from "../element/textElement"; import { normalizeLink } from "./url"; +import { syncInvalidIndices } from "../fractionalIndex"; type RestoredAppState = Omit< AppState, @@ -73,7 +74,7 @@ export const AllowedExcalidrawActiveTools: Record< }; export type RestoredDataState = { - elements: ExcalidrawElement[]; + elements: OrderedExcalidrawElement[]; appState: RestoredAppState; files: BinaryFiles; }; @@ -101,8 +102,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, keyof ExcalidrawElement>>, >( @@ -115,14 +114,13 @@ const restoreElementWithProperties = < > & Partial>, ): T => { - const base: Pick & { - [PRECEDING_ELEMENT_KEY]?: string; - } = { + const base: Pick = { 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, + index: element.index ?? null, 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), @@ -407,30 +401,35 @@ export const restoreElements = ( /** NOTE doesn't serve for reconciliation */ localElements: readonly ExcalidrawElement[] | null | undefined, opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined, -): ExcalidrawElement[] => { +): OrderedExcalidrawElement[] => { // used to detect duplicate top-level element ids const existingIds = new Set(); const localElementsMap = localElements ? arrayToMap(localElements) : null; - const restoredElements = (elements || []).reduce((elements, element) => { - // filtering out selection, which is legacy, no longer kept in elements, - // and causing issues if retained - if (element.type !== "selection" && !isInvisiblySmallElement(element)) { - let migratedElement: ExcalidrawElement | null = restoreElement(element); - if (migratedElement) { - const localElement = localElementsMap?.get(element.id); - if (localElement && localElement.version > migratedElement.version) { - migratedElement = bumpVersion(migratedElement, localElement.version); - } - if (existingIds.has(migratedElement.id)) { - migratedElement = { ...migratedElement, id: randomId() }; - } - existingIds.add(migratedElement.id); + const restoredElements = syncInvalidIndices( + (elements || []).reduce((elements, element) => { + // filtering out selection, which is legacy, no longer kept in elements, + // and causing issues if retained + if (element.type !== "selection" && !isInvisiblySmallElement(element)) { + let migratedElement: ExcalidrawElement | null = restoreElement(element); + if (migratedElement) { + const localElement = localElementsMap?.get(element.id); + if (localElement && localElement.version > migratedElement.version) { + migratedElement = bumpVersion( + migratedElement, + localElement.version, + ); + } + if (existingIds.has(migratedElement.id)) { + migratedElement = { ...migratedElement, id: randomId() }; + } + existingIds.add(migratedElement.id); - elements.push(migratedElement); + elements.push(migratedElement); + } } - } - return elements; - }, [] as ExcalidrawElement[]); + return elements; + }, [] as ExcalidrawElement[]), + ); if (!opts?.repairBindings) { return restoredElements; diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts index 936272f07eb50..9706f849fdd56 100644 --- a/packages/excalidraw/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -44,9 +44,16 @@ import { VerticalAlign, } from "../element/types"; import { MarkOptional } from "../utility-types"; -import { assertNever, cloneJSON, getFontString, toBrandedType } from "../utils"; +import { + arrayToMap, + assertNever, + cloneJSON, + getFontString, + toBrandedType, +} from "../utils"; import { getSizeFromPoints } from "../points"; import { randomId } from "../random"; +import { syncInvalidIndices } from "../fractionalIndex"; export type ValidLinearElement = { type: "arrow" | "line"; @@ -457,12 +464,15 @@ class ElementStore { this.excalidrawElements.set(ele.id, ele); }; + getElements = () => { - return Array.from(this.excalidrawElements.values()); + return syncInvalidIndices(Array.from(this.excalidrawElements.values())); }; getElementsMap = () => { - return toBrandedType(this.excalidrawElements); + return toBrandedType( + arrayToMap(this.getElements()), + ); }; getElement = (id: string) => { diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index 967359c5a253b..34dec0adb31ad 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -55,6 +55,7 @@ export type ElementConstructorOpts = MarkOptional< | "angle" | "groupIds" | "frameId" + | "index" | "boundElements" | "seed" | "version" @@ -89,6 +90,7 @@ const _newElementBase = ( angle = 0, groupIds = [], frameId = null, + index = null, roundness = null, boundElements = null, link = null, @@ -114,6 +116,7 @@ const _newElementBase = ( opacity, groupIds, frameId, + index, roundness, seed: rest.seed ?? randomInteger(), version: rest.version || 1, diff --git a/packages/excalidraw/element/textWysiwyg.test.tsx b/packages/excalidraw/element/textWysiwyg.test.tsx index 478fe5c1afebe..2d38b82131584 100644 --- a/packages/excalidraw/element/textWysiwyg.test.tsx +++ b/packages/excalidraw/element/textWysiwyg.test.tsx @@ -1454,7 +1454,7 @@ describe("textWysiwyg", () => { strokeWidth: 2, type: "rectangle", updated: 1, - version: 1, + version: 2, width: 610, x: 15, y: 25, diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index 85f42adcf3037..8d3a0d4ef7be8 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -24,6 +24,7 @@ export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN]; type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN; export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys]; +export type FractionalIndex = string & { _brand: "franctionalIndex" }; type _ExcalidrawElementBase = Readonly<{ id: string; @@ -50,6 +51,11 @@ type _ExcalidrawElementBase = Readonly<{ Used for deterministic reconciliation of updates during collaboration, in case the versions (see above) are identical. */ versionNonce: number; + /** String in a fractional form defined by https://github.com/rocicorp/fractional-indexing. + Used for ordering in multiplayer scenarios, such as during reconciliation or undo / redo. + Always kept in sync with the array order by `syncMovedIndices` and `syncInvalidIndices`. + Could be null, i.e. for new elements which were not yet assigned to the scene. */ + index: FractionalIndex | null; isDeleted: boolean; /** List of groups the element belongs to. Ordered from deepest to shallowest. */ @@ -164,6 +170,12 @@ export type ExcalidrawElement = | ExcalidrawIframeElement | ExcalidrawEmbeddableElement; +export type Ordered = TElement & { + index: FractionalIndex; +}; + +export type OrderedExcalidrawElement = Ordered; + export type NonDeleted = TElement & { isDeleted: boolean; }; @@ -275,7 +287,10 @@ export type NonDeletedElementsMap = Map< * Map of all excalidraw Scene elements, including deleted. * Not a subset. Use this type when you need access to current Scene elements. */ -export type SceneElementsMap = Map & +export type SceneElementsMap = Map< + ExcalidrawElement["id"], + Ordered +> & MakeBrand<"SceneElementsMap">; /** @@ -284,7 +299,7 @@ export type SceneElementsMap = Map & */ export type NonDeletedSceneElementsMap = Map< ExcalidrawElement["id"], - NonDeletedExcalidrawElement + Ordered > & MakeBrand<"NonDeletedSceneElementsMap">; diff --git a/packages/excalidraw/errors.ts b/packages/excalidraw/errors.ts index 4df403496608b..705ba7a6ff8a1 100644 --- a/packages/excalidraw/errors.ts +++ b/packages/excalidraw/errors.ts @@ -32,3 +32,7 @@ export class ImageSceneDataError extends Error { this.code = code; } } + +export class InvalidFractionalIndexError extends Error { + public code = "ELEMENT_HAS_INVALID_INDEX" as const; +} diff --git a/packages/excalidraw/fractionalIndex.ts b/packages/excalidraw/fractionalIndex.ts new file mode 100644 index 0000000000000..ccda1d365df00 --- /dev/null +++ b/packages/excalidraw/fractionalIndex.ts @@ -0,0 +1,348 @@ +import { generateNKeysBetween } from "fractional-indexing"; +import { mutateElement } from "./element/mutateElement"; +import { + ExcalidrawElement, + FractionalIndex, + OrderedExcalidrawElement, +} from "./element/types"; +import { InvalidFractionalIndexError } from "./errors"; + +/** + * Envisioned relation between array order and fractional indices: + * + * 1) Array (or array-like ordered data structure) should be used as a cache of elements order, hiding the internal fractional indices implementation. + * - it's undesirable to to perform reorder for each related operation, thefeore it's necessary to cache the order defined by fractional indices into an ordered data structure + * - it's easy enough to define the order of the elements from the outside (boundaries), without worrying about the underlying structure of fractional indices (especially for the host apps) + * - it's necessary to always keep the array support for backwards compatibility (restore) - old scenes, old libraries, supporting multiple excalidraw versions etc. + * - it's necessary to always keep the fractional indices in sync with the array order + * - elements with invalid indices should be detected and synced, without altering the already valid indices + * + * 2) Fractional indices should be used to reorder the elements, whenever the cached order is expected to be invalidated. + * - as the fractional indices are encoded as part of the elements, it opens up possibilties for incremental-like APIs + * - re-order based on fractional indices should be part of (multiplayer) operations such as reconcillitation & undo/redo + * - technically all the z-index actions could perform also re-order based on fractional indices,but in current state it would not bring much benefits, + * as it's faster & more efficient to perform re-order based on array manipulation and later synchronisation of moved indices with the array order + */ + +/** + * Ensure that all elements have valid fractional indices. + * + * @throws `InvalidFractionalIndexError` if invalid index is detected. + */ +export const validateFractionalIndices = ( + indices: (ExcalidrawElement["index"] | undefined)[], +) => { + for (const [i, index] of indices.entries()) { + const predecessorIndex = indices[i - 1]; + const successorIndex = indices[i + 1]; + + if (!isValidFractionalIndex(index, predecessorIndex, successorIndex)) { + throw new InvalidFractionalIndexError( + `Fractional indices invariant for element has been compromised - ["${predecessorIndex}", "${index}", "${successorIndex}"] [predecessor, current, successor]`, + ); + } + } +}; + +/** + * Order the elements based on the fractional indices. + * - when fractional indices are identical, break the tie based on the element id + * - when there is no fractional index in one of the elements, respect the order of the array + */ +export const orderByFractionalIndex = ( + elements: OrderedExcalidrawElement[], +) => { + return elements.sort((a, b) => { + // in case the indices are not the defined at runtime + if (isOrderedElement(a) && isOrderedElement(b)) { + if (a.index < b.index) { + return -1; + } else if (a.index > b.index) { + return 1; + } + + // break ties based on the element id + return a.id < b.id ? -1 : 1; + } + + // defensively keep the array order + return 1; + }); +}; + +/** + * Synchronizes invalid fractional indices of moved elements with the array order by mutating passed elements. + * If the synchronization fails or the result is invalid, it fallbacks to `syncInvalidIndices`. + */ +export const syncMovedIndices = ( + elements: readonly ExcalidrawElement[], + movedElements: Map, +): OrderedExcalidrawElement[] => { + try { + const indicesGroups = getMovedIndicesGroups(elements, movedElements); + + // try generatating indices, throws on invalid movedElements + const elementsUpdates = generateIndices(elements, indicesGroups); + + // ensure next indices are valid before mutation, throws on invalid ones + validateFractionalIndices( + elements.map((x) => elementsUpdates.get(x)?.index || x.index), + ); + + // split mutation so we don't end up in an incosistent state + for (const [element, update] of elementsUpdates) { + mutateElement(element, update, false); + } + } catch (e) { + // fallback to default sync + syncInvalidIndices(elements); + } + + return elements as OrderedExcalidrawElement[]; +}; + +/** + * Synchronizes all invalid fractional indices with the array order by mutating passed elements. + * + * WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself. + */ +export const syncInvalidIndices = ( + elements: readonly ExcalidrawElement[], +): OrderedExcalidrawElement[] => { + const indicesGroups = getInvalidIndicesGroups(elements); + const elementsUpdates = generateIndices(elements, indicesGroups); + + for (const [element, update] of elementsUpdates) { + mutateElement(element, update, false); + } + + return elements as OrderedExcalidrawElement[]; +}; + +/** + * Get contiguous groups of indices of passed moved elements. + * + * NOTE: First and last elements within the groups are indices of lower and upper bounds. + */ +const getMovedIndicesGroups = ( + elements: readonly ExcalidrawElement[], + movedElements: Map, +) => { + const indicesGroups: number[][] = []; + + let i = 0; + + while (i < elements.length) { + if ( + movedElements.has(elements[i].id) && + !isValidFractionalIndex( + elements[i]?.index, + elements[i - 1]?.index, + elements[i + 1]?.index, + ) + ) { + const indicesGroup = [i - 1, i]; // push the lower bound index as the first item + + while (++i < elements.length) { + if ( + !( + movedElements.has(elements[i].id) && + !isValidFractionalIndex( + elements[i]?.index, + elements[i - 1]?.index, + elements[i + 1]?.index, + ) + ) + ) { + break; + } + + indicesGroup.push(i); + } + + indicesGroup.push(i); // push the upper bound index as the last item + indicesGroups.push(indicesGroup); + } else { + i++; + } + } + + return indicesGroups; +}; + +/** + * Gets contiguous groups of all invalid indices automatically detected inside the elements array. + * + * WARN: First and last items within the groups do NOT have to be contiguous, those are the found lower and upper bounds! + */ +const getInvalidIndicesGroups = (elements: readonly ExcalidrawElement[]) => { + const indicesGroups: number[][] = []; + + // once we find lowerBound / upperBound, it cannot be lower than that, so we cache it for better perf. + let lowerBound: ExcalidrawElement["index"] | undefined = undefined; + let upperBound: ExcalidrawElement["index"] | undefined = undefined; + let lowerBoundIndex: number = -1; + let upperBoundIndex: number = 0; + + /** @returns maybe valid lowerBound */ + const getLowerBound = ( + index: number, + ): [ExcalidrawElement["index"] | undefined, number] => { + const lowerBound = elements[lowerBoundIndex] + ? elements[lowerBoundIndex].index + : undefined; + + // we are already iterating left to right, therefore there is no need for additional looping + const candidate = elements[index - 1]?.index; + + if ( + (!lowerBound && candidate) || // first lowerBound + (lowerBound && candidate && candidate > lowerBound) // next lowerBound + ) { + // WARN: candidate's index could be higher or same as the current element's index + return [candidate, index - 1]; + } + + // cache hit! take the last lower bound + return [lowerBound, lowerBoundIndex]; + }; + + /** @returns always valid upperBound */ + const getUpperBound = ( + index: number, + ): [ExcalidrawElement["index"] | undefined, number] => { + const upperBound = elements[upperBoundIndex] + ? elements[upperBoundIndex].index + : undefined; + + // cache hit! don't let it find the upper bound again + if (upperBound && index < upperBoundIndex) { + return [upperBound, upperBoundIndex]; + } + + // set the current upperBoundIndex as the starting point + let i = upperBoundIndex; + while (++i < elements.length) { + const candidate = elements[i]?.index; + + if ( + (!upperBound && candidate) || // first upperBound + (upperBound && candidate && candidate > upperBound) // next upperBound + ) { + return [candidate, i]; + } + } + + // we reached the end, sky is the limit + return [undefined, i]; + }; + + let i = 0; + + while (i < elements.length) { + const current = elements[i].index; + [lowerBound, lowerBoundIndex] = getLowerBound(i); + [upperBound, upperBoundIndex] = getUpperBound(i); + + if (!isValidFractionalIndex(current, lowerBound, upperBound)) { + // push the lower bound index as the first item + const indicesGroup = [lowerBoundIndex, i]; + + while (++i < elements.length) { + const current = elements[i].index; + const [nextLowerBound, nextLowerBoundIndex] = getLowerBound(i); + const [nextUpperBound, nextUpperBoundIndex] = getUpperBound(i); + + if (isValidFractionalIndex(current, nextLowerBound, nextUpperBound)) { + break; + } + + // assign bounds only for the moved elements + [lowerBound, lowerBoundIndex] = [nextLowerBound, nextLowerBoundIndex]; + [upperBound, upperBoundIndex] = [nextUpperBound, nextUpperBoundIndex]; + + indicesGroup.push(i); + } + + // push the upper bound index as the last item + indicesGroup.push(upperBoundIndex); + indicesGroups.push(indicesGroup); + } else { + i++; + } + } + + return indicesGroups; +}; + +const isValidFractionalIndex = ( + index: ExcalidrawElement["index"] | undefined, + predecessor: ExcalidrawElement["index"] | undefined, + successor: ExcalidrawElement["index"] | undefined, +) => { + if (!index) { + return false; + } + + if (predecessor && successor) { + return predecessor < index && index < successor; + } + + if (!predecessor && successor) { + // first element + return index < successor; + } + + if (predecessor && !successor) { + // last element + return predecessor < index; + } + + // only element in the array + return !!index; +}; + +const generateIndices = ( + elements: readonly ExcalidrawElement[], + indicesGroups: number[][], +) => { + const elementsUpdates = new Map< + ExcalidrawElement, + { index: FractionalIndex } + >(); + + for (const indices of indicesGroups) { + const lowerBoundIndex = indices.shift()!; + const upperBoundIndex = indices.pop()!; + + const fractionalIndices = generateNKeysBetween( + elements[lowerBoundIndex]?.index, + elements[upperBoundIndex]?.index, + indices.length, + ) as FractionalIndex[]; + + for (let i = 0; i < indices.length; i++) { + const element = elements[indices[i]]; + + elementsUpdates.set(element, { + index: fractionalIndices[i], + }); + } + } + + return elementsUpdates; +}; + +const isOrderedElement = ( + element: ExcalidrawElement, +): element is OrderedExcalidrawElement => { + // for now it's sufficient whether the index is there + // meaning, the element was already ordered in the past + // meaning, it is not a newly inserted element, not an unrestored element, etc. + // it does not have to mean that the index itself is valid + if (element.index) { + return true; + } + + return false; +}; diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index d627fc4c950c2..c2e7aa1626e32 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -29,7 +29,7 @@ import { ReadonlySetLike } from "./utility-types"; // --------------------------- Frame State ------------------------------------ export const bindElementsToFramesAfterDuplication = ( - nextElements: ExcalidrawElement[], + nextElements: readonly ExcalidrawElement[], oldElements: readonly ExcalidrawElement[], oldIdToDuplicatedId: Map, ) => { diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index ad8ffcf889b87..8b3e41acda077 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -49,6 +49,7 @@ "canvas-roundrect-polyfill": "0.0.1", "clsx": "1.1.1", "cross-env": "7.0.3", + "fractional-indexing": "3.2.0", "image-blob-reduce": "3.0.1", "jotai": "1.13.1", "lodash.throttle": "4.1.1", @@ -76,6 +77,8 @@ "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "7.18.6", "@size-limit/preset-big-lib": "9.0.0", + "@testing-library/jest-dom": "5.16.2", + "@testing-library/react": "12.1.5", "@types/pako": "1.0.3", "@types/pica": "5.1.3", "@types/resize-observer-browser": "0.1.7", @@ -98,8 +101,6 @@ "sass-loader": "13.0.2", "size-limit": "9.0.0", "style-loader": "3.3.3", - "@testing-library/jest-dom": "5.16.2", - "@testing-library/react": "12.1.5", "ts-loader": "9.3.1", "typescript": "4.9.4" }, diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts index c76b81a822d1b..6be7d870480cd 100644 --- a/packages/excalidraw/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -6,6 +6,8 @@ import { ElementsMapOrArray, SceneElementsMap, NonDeletedSceneElementsMap, + OrderedExcalidrawElement, + Ordered, } from "../element/types"; import { isNonDeletedElement } from "../element"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -14,7 +16,14 @@ import { getSelectedElements } from "./selection"; import { AppState } from "../types"; import { Assert, SameType } from "../utility-types"; import { randomInteger } from "../random"; +import { + syncInvalidIndices, + syncMovedIndices, + validateFractionalIndices, +} from "../fractionalIndex"; +import { arrayToMap } from "../utils"; import { toBrandedType } from "../utils"; +import { ENV } from "../constants"; type ElementIdKey = InstanceType["elementId"]; type ElementKey = ExcalidrawElement | ElementIdKey; @@ -32,7 +41,10 @@ const getNonDeletedElements = ( for (const element of allElements) { if (!element.isDeleted) { elements.push(element as NonDeleted); - elementsMap.set(element.id, element as NonDeletedExcalidrawElement); + elementsMap.set( + element.id, + element as Ordered, + ); } } return { elementsMap, elements }; @@ -106,11 +118,13 @@ class Scene { private callbacks: Set = new Set(); - private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = []; + private nonDeletedElements: readonly Ordered[] = + []; private nonDeletedElementsMap = toBrandedType( new Map(), ); - private elements: readonly ExcalidrawElement[] = []; + // ideally all elements within the scene should be wrapped around with `Ordered` type, but right now there is no real benefit doing so + private elements: readonly OrderedExcalidrawElement[] = []; private nonDeletedFramesLikes: readonly NonDeleted[] = []; private frames: readonly ExcalidrawFrameLikeElement[] = []; @@ -138,7 +152,7 @@ class Scene { return this.elements; } - getNonDeletedElements(): readonly NonDeletedExcalidrawElement[] { + getNonDeletedElements() { return this.nonDeletedElements; } @@ -244,12 +258,19 @@ class Scene { } replaceAllElements(nextElements: ElementsMapOrArray) { - this.elements = + const _nextElements = // ts doesn't like `Array.isArray` of `instanceof Map` nextElements instanceof Array ? nextElements : Array.from(nextElements.values()); const nextFrameLikes: ExcalidrawFrameLikeElement[] = []; + + if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) { + // throw on invalid indices in test / dev to potentially detect cases were we forgot to sync moved elements + validateFractionalIndices(_nextElements.map((x) => x.index)); + } + + this.elements = syncInvalidIndices(_nextElements); this.elementsMap.clear(); this.elements.forEach((element) => { if (isFrameLikeElement(element)) { @@ -292,8 +313,8 @@ class Scene { } destroy() { - this.nonDeletedElements = []; this.elements = []; + this.nonDeletedElements = []; this.nonDeletedFramesLikes = []; this.frames = []; this.elementsMap.clear(); @@ -318,11 +339,15 @@ class Scene { "insertElementAtIndex can only be called with index >= 0", ); } + const nextElements = [ ...this.elements.slice(0, index), element, ...this.elements.slice(index), ]; + + syncMovedIndices(nextElements, arrayToMap([element])); + this.replaceAllElements(nextElements); } @@ -332,21 +357,32 @@ class Scene { "insertElementAtIndex can only be called with index >= 0", ); } + const nextElements = [ ...this.elements.slice(0, index), ...elements, ...this.elements.slice(index), ]; + syncMovedIndices(nextElements, arrayToMap(elements)); + this.replaceAllElements(nextElements); } - addNewElement = (element: ExcalidrawElement) => { - if (element.frameId) { - this.insertElementAtIndex(element, this.getElementIndex(element.frameId)); - } else { - this.replaceAllElements([...this.elements, element]); - } + insertElement = (element: ExcalidrawElement) => { + const index = element.frameId + ? this.getElementIndex(element.frameId) + : this.elements.length; + + this.insertElementAtIndex(element, index); + }; + + insertElements = (elements: ExcalidrawElement[]) => { + const index = elements[0].frameId + ? this.getElementIndex(elements[0].frameId) + : this.elements.length; + + this.insertElementsAtIndex(elements, index); }; getElementIndex(elementId: string) { diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index 5fd8ea5db44d6..94d188a7ce2db 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -43,6 +43,7 @@ import { Mutable } from "../utility-types"; import { newElementWith } from "../element/mutateElement"; import { isFrameElement, isFrameLikeElement } from "../element/typeChecks"; import { RenderableElementsMap } from "./types"; +import { syncInvalidIndices } from "../fractionalIndex"; import { renderStaticScene } from "../renderer/staticScene"; const SVG_EXPORT_TAG = ``; @@ -590,7 +591,7 @@ export const exportToCanvas = async ({ arrayToMap(elementsForRender), ), allElementsMap: toBrandedType( - arrayToMap(elements), + arrayToMap(syncInvalidIndices(elements)), ), visibleElements: elementsForRender, appState: { diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 682af4bfe8728..594dc5981cb49 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -395,6 +395,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro ], "height": 100, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -409,8 +410,8 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 453191, "width": 100, "x": 0, "y": 0, @@ -430,6 +431,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro ], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -444,8 +446,8 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 401146281, "width": 100, "x": 0, "y": 0, @@ -592,6 +594,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -606,8 +609,8 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -652,6 +655,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -666,8 +670,8 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -796,6 +800,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -804,14 +809,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -829,6 +834,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -843,8 +849,8 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": -10, "y": 0, @@ -889,6 +895,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -903,8 +910,8 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -933,6 +940,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -947,8 +955,8 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -963,6 +971,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -971,14 +980,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -1007,6 +1016,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1015,14 +1025,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -1037,6 +1047,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1051,8 +1062,8 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": -10, "y": 0, @@ -1181,6 +1192,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1189,14 +1201,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -1214,6 +1226,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1228,8 +1241,8 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": -10, "y": 0, @@ -1274,6 +1287,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1288,8 +1302,8 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -1318,6 +1332,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1332,8 +1347,8 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -1348,6 +1363,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1356,14 +1372,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -1392,6 +1408,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1400,14 +1417,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -1422,6 +1439,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "groupIds": [], "height": 20, "id": "id0", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1436,8 +1454,8 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": -10, "y": 0, @@ -1568,6 +1586,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1582,8 +1601,8 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -1628,6 +1647,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1642,8 +1662,8 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -1770,6 +1790,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": true, "link": null, "locked": false, @@ -1784,8 +1805,8 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1116226695, + "version": 4, + "versionNonce": 1014066025, "width": 20, "x": -10, "y": 0, @@ -1830,6 +1851,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1844,8 +1866,8 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -1872,6 +1894,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": true, "link": null, "locked": false, @@ -1886,8 +1909,8 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1116226695, + "version": 4, + "versionNonce": 1014066025, "width": 20, "x": -10, "y": 0, @@ -2016,6 +2039,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2030,8 +2054,8 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2049,6 +2073,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "groupIds": [], "height": 20, "id": "id0_copy", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2057,14 +2082,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 4, + "versionNonce": 238820263, "width": 20, "x": 0, "y": 10, @@ -2109,6 +2134,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2123,8 +2149,8 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2153,6 +2179,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2167,8 +2194,8 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2183,6 +2210,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "groupIds": [], "height": 20, "id": "id0_copy", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2191,14 +2219,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 4, + "versionNonce": 238820263, "width": 20, "x": 0, "y": 10, @@ -2334,6 +2362,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group ], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2348,8 +2377,8 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 20, "x": -10, "y": 0, @@ -2369,6 +2398,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group ], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2377,14 +2407,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 23633383, + "version": 4, + "versionNonce": 915032327, "width": 20, "x": 20, "y": 30, @@ -2429,6 +2459,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2443,8 +2474,8 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2473,6 +2504,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2487,8 +2519,8 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2503,6 +2535,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2511,14 +2544,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -2552,6 +2585,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group ], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2566,8 +2600,8 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 20, "x": -10, "y": 0, @@ -2584,6 +2618,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group ], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2592,14 +2627,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 23633383, + "version": 4, + "versionNonce": 915032327, "width": 20, "x": 20, "y": 30, @@ -2730,6 +2765,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2744,8 +2780,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 640725609, + "version": 4, + "versionNonce": 941653321, "width": 20, "x": -10, "y": 0, @@ -2763,6 +2799,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2771,14 +2808,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 760410951, + "seed": 289600103, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 8, - "versionNonce": 1315507081, + "version": 9, + "versionNonce": 640725609, "width": 20, "x": 20, "y": 30, @@ -2823,6 +2860,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2837,8 +2875,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2867,6 +2905,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2881,8 +2920,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2897,6 +2936,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2905,14 +2945,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -2941,6 +2981,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2955,8 +2996,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -2971,6 +3012,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2979,14 +3021,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#e03131", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -3015,6 +3057,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3029,8 +3072,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3045,6 +3088,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3053,14 +3097,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#e03131", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 23633383, + "version": 5, + "versionNonce": 915032327, "width": 20, "x": 20, "y": 30, @@ -3089,6 +3133,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3103,8 +3148,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3119,6 +3164,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3127,14 +3173,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#e03131", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, - "versionNonce": 915032327, + "version": 6, + "versionNonce": 747212839, "width": 20, "x": 20, "y": 30, @@ -3163,6 +3209,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3177,8 +3224,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3193,6 +3240,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3201,14 +3249,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 6, - "versionNonce": 747212839, + "version": 7, + "versionNonce": 760410951, "width": 20, "x": 20, "y": 30, @@ -3237,6 +3285,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3251,8 +3300,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3267,6 +3316,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3275,14 +3325,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 760410951, + "seed": 289600103, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 7, - "versionNonce": 1006504105, + "version": 8, + "versionNonce": 1315507081, "width": 20, "x": 20, "y": 30, @@ -3311,6 +3361,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3325,8 +3376,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3341,6 +3392,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3349,14 +3401,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 760410951, + "seed": 289600103, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 8, - "versionNonce": 1315507081, + "version": 9, + "versionNonce": 640725609, "width": 20, "x": 20, "y": 30, @@ -3385,6 +3437,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3399,8 +3452,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 640725609, + "version": 4, + "versionNonce": 941653321, "width": 20, "x": -10, "y": 0, @@ -3415,6 +3468,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3423,14 +3477,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 760410951, + "seed": 289600103, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 8, - "versionNonce": 1315507081, + "version": 9, + "versionNonce": 640725609, "width": 20, "x": 20, "y": 30, @@ -3559,6 +3613,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "groupIds": [], "height": 20, "id": "id1", + "index": "Zz", "isDeleted": false, "link": null, "locked": false, @@ -3567,14 +3622,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -3592,6 +3647,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3606,8 +3662,8 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3652,6 +3708,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3666,8 +3723,8 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3696,6 +3753,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3710,8 +3768,8 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3726,6 +3784,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3734,14 +3793,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -3770,6 +3829,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "groupIds": [], "height": 20, "id": "id1", + "index": "Zz", "isDeleted": false, "link": null, "locked": false, @@ -3778,14 +3838,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -3800,6 +3860,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3814,8 +3875,8 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -3944,6 +4005,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "groupIds": [], "height": 20, "id": "id1", + "index": "Zz", "isDeleted": false, "link": null, "locked": false, @@ -3952,14 +4014,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -3977,6 +4039,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3991,8 +4054,8 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -4037,6 +4100,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4051,8 +4115,8 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -4081,6 +4145,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4095,8 +4160,8 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -4111,6 +4176,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4119,14 +4185,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -4155,6 +4221,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "groupIds": [], "height": 20, "id": "id1", + "index": "Zz", "isDeleted": false, "link": null, "locked": false, @@ -4163,14 +4230,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -4185,6 +4252,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4199,8 +4267,8 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -4332,6 +4400,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4346,8 +4415,8 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 81784553, + "version": 5, + "versionNonce": 1723083209, "width": 20, "x": -10, "y": 0, @@ -4365,6 +4434,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4373,14 +4443,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 747212839, + "version": 5, + "versionNonce": 760410951, "width": 20, "x": 20, "y": 30, @@ -4425,6 +4495,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4439,8 +4510,8 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -4469,6 +4540,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4483,8 +4555,8 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -4499,6 +4571,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4507,14 +4580,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 400692809, + "version": 3, + "versionNonce": 1505387817, "width": 20, "x": 20, "y": 30, @@ -4548,6 +4621,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung ], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4562,8 +4636,8 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 23633383, + "version": 4, + "versionNonce": 915032327, "width": 20, "x": -10, "y": 0, @@ -4580,6 +4654,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung ], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4588,14 +4663,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 81784553, "width": 20, "x": 20, "y": 30, @@ -4625,6 +4700,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4639,8 +4715,8 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 81784553, + "version": 5, + "versionNonce": 1723083209, "width": 20, "x": -10, "y": 0, @@ -4655,6 +4731,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4663,14 +4740,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 747212839, + "version": 5, + "versionNonce": 760410951, "width": 20, "x": 20, "y": 30, @@ -5075,6 +5152,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5089,8 +5167,8 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 1014066025, "width": 10, "x": -10, "y": 0, @@ -5108,6 +5186,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5116,14 +5195,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 0, @@ -5168,6 +5247,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5182,8 +5262,8 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 1014066025, "width": 10, "x": -10, "y": 0, @@ -5212,6 +5292,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5226,8 +5307,8 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 1014066025, "width": 10, "x": -10, "y": 0, @@ -5242,6 +5323,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5250,14 +5332,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 0, @@ -5666,6 +5748,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5680,8 +5763,8 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 81784553, "width": 10, "x": -10, "y": 0, @@ -5701,6 +5784,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5709,14 +5793,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 747212839, "width": 10, "x": 10, "y": 0, @@ -5761,6 +5845,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5775,8 +5860,8 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 10, "x": -10, "y": 0, @@ -5805,6 +5890,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5819,8 +5905,8 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 10, "x": -10, "y": 0, @@ -5835,6 +5921,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5843,14 +5930,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 400692809, + "version": 3, + "versionNonce": 1505387817, "width": 10, "x": 10, "y": 0, @@ -5884,6 +5971,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5898,8 +5986,8 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 81784553, "width": 10, "x": -10, "y": 0, @@ -5916,6 +6004,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5924,14 +6013,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 747212839, "width": 10, "x": 10, "y": 0, @@ -6967,6 +7056,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6981,8 +7071,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -7000,6 +7090,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "groupIds": [], "height": 200, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7014,8 +7105,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 453191, "width": 200, "x": 0, "y": 0, @@ -7033,6 +7124,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "groupIds": [], "height": 200, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7047,8 +7139,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 401146281, "width": 200, "x": 0, "y": 0, @@ -7093,6 +7185,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] hi "groupIds": [], "height": 20, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7107,8 +7200,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] hi "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 2019559783, + "version": 3, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, diff --git a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap index 91203eefba91f..ab705360fd26c 100644 --- a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap @@ -15,6 +15,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -42,8 +43,8 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 30, "x": 30, "y": 20, @@ -63,6 +64,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -77,8 +79,8 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 30, "x": 30, "y": 20, @@ -98,6 +100,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -112,8 +115,8 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 30, "x": 30, "y": 20, @@ -133,6 +136,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -160,8 +164,8 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 30, "x": 30, "y": 20, @@ -181,6 +185,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -195,8 +200,8 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 30, "x": 30, "y": 20, diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index f287e547ba865..39ad72b24c2cb 100644 --- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -11,6 +11,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = ` "groupIds": [], "height": 50, "id": "id0_copy", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -19,14 +20,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = ` "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 238820263, + "version": 5, + "versionNonce": 400692809, "width": 30, "x": 30, "y": 20, @@ -44,6 +45,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = ` "groupIds": [], "height": 50, "id": "id0", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -58,8 +60,8 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1604849351, + "version": 6, + "versionNonce": 23633383, "width": 30, "x": -10, "y": 60, @@ -77,6 +79,7 @@ exports[`move element > rectangle 5`] = ` "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -91,8 +94,8 @@ exports[`move element > rectangle 5`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1150084233, + "version": 4, + "versionNonce": 1116226695, "width": 30, "x": 0, "y": 40, @@ -115,6 +118,7 @@ exports[`move element > rectangles with binding arrow 5`] = ` "groupIds": [], "height": 100, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -129,8 +133,8 @@ exports[`move element > rectangles with binding arrow 5`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 760410951, "width": 100, "x": 0, "y": 0, @@ -153,6 +157,7 @@ exports[`move element > rectangles with binding arrow 6`] = ` "groupIds": [], "height": 300, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -161,14 +166,14 @@ exports[`move element > rectangles with binding arrow 6`] = ` "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 6, - "versionNonce": 927333447, + "version": 7, + "versionNonce": 745419401, "width": 300, "x": 201, "y": 2, @@ -192,6 +197,7 @@ exports[`move element > rectangles with binding arrow 7`] = ` "groupIds": [], "height": 81.48231043525051, "id": "id2", + "index": "a2", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -211,7 +217,7 @@ exports[`move element > rectangles with binding arrow 7`] = ` "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "startArrowhead": null, "startBinding": { "elementId": "id0", @@ -223,8 +229,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` "strokeWidth": 2, "type": "line", "updated": 1, - "version": 11, - "versionNonce": 1051383431, + "version": 12, + "versionNonce": 1984422985, "width": 81, "x": 110, "y": 49.981789081137734, diff --git a/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap index 3697f91b1420c..d7602f15f0ef0 100644 --- a/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap @@ -13,6 +13,7 @@ exports[`multi point mode in linear elements > arrow 3`] = ` "groupIds": [], "height": 110, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": [ 70, @@ -47,8 +48,8 @@ exports[`multi point mode in linear elements > arrow 3`] = ` "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 7, - "versionNonce": 1505387817, + "version": 8, + "versionNonce": 23633383, "width": 70, "x": 30, "y": 30, @@ -68,6 +69,7 @@ exports[`multi point mode in linear elements > line 3`] = ` "groupIds": [], "height": 110, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": [ 70, @@ -102,8 +104,8 @@ exports[`multi point mode in linear elements > line 3`] = ` "strokeWidth": 2, "type": "line", "updated": 1, - "version": 7, - "versionNonce": 1505387817, + "version": 8, + "versionNonce": 23633383, "width": 70, "x": 30, "y": 30, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index de116643246e8..13ec543817383 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -148,6 +148,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -162,8 +163,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -192,6 +193,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -206,8 +208,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -222,6 +224,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -230,14 +233,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 0, "y": 30, @@ -266,6 +269,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -280,8 +284,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -296,6 +300,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -304,14 +309,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 0, "y": 30, @@ -326,6 +331,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -334,14 +340,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 10, "x": 0, "y": 60, @@ -373,6 +379,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -381,14 +388,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 0, "y": 30, @@ -405,6 +412,7 @@ exports[`given element A and group of elements B and given both are selected whe ], "height": 10, "id": "id0", + "index": "a1V", "isDeleted": false, "link": null, "locked": false, @@ -419,8 +427,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 5, + "versionNonce": 1006504105, "width": 10, "x": 0, "y": 0, @@ -437,6 +445,7 @@ exports[`given element A and group of elements B and given both are selected whe ], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -445,14 +454,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 760410951, "width": 10, "x": 0, "y": 60, @@ -617,6 +626,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 100, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -631,8 +641,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 100, "x": 0, "y": 0, @@ -661,6 +671,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 100, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -675,8 +686,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 100, "x": 0, "y": 0, @@ -691,6 +702,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -699,14 +711,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 100, "x": 110, "y": 110, @@ -735,6 +747,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 100, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -749,8 +762,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 100, "x": 0, "y": 0, @@ -765,6 +778,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -773,14 +787,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 100, "x": 110, "y": 110, @@ -795,6 +809,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 100, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -803,14 +818,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 100, "x": 220, "y": 220, @@ -842,6 +857,7 @@ exports[`given element A and group of elements B and given both are selected whe "groupIds": [], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -850,14 +866,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 100, "x": 110, "y": 110, @@ -874,6 +890,7 @@ exports[`given element A and group of elements B and given both are selected whe ], "height": 100, "id": "id0", + "index": "a1V", "isDeleted": false, "link": null, "locked": false, @@ -888,8 +905,8 @@ exports[`given element A and group of elements B and given both are selected whe "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 5, + "versionNonce": 760410951, "width": 100, "x": 0, "y": 0, @@ -906,6 +923,7 @@ exports[`given element A and group of elements B and given both are selected whe ], "height": 100, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -914,14 +932,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 1723083209, "width": 100, "x": 220, "y": 220, @@ -1077,6 +1095,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1091,8 +1110,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -1121,6 +1140,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1135,8 +1155,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -1151,6 +1171,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1159,14 +1180,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 0, @@ -1200,6 +1221,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1214,8 +1236,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 0, "y": 0, @@ -1232,6 +1254,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1240,14 +1263,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 30, "y": 0, @@ -1278,6 +1301,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1292,8 +1316,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 0, "y": 0, @@ -1310,6 +1334,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1318,14 +1343,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 30, "y": 0, @@ -1356,6 +1381,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1370,8 +1396,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 0, "y": 0, @@ -1388,6 +1414,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1396,14 +1423,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 30, "y": 0, @@ -1418,6 +1445,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [], "height": 10, "id": "id7", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1426,14 +1454,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 81784553, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1723083209, + "version": 3, + "versionNonce": 289600103, "width": 10, "x": 60, "y": 0, @@ -1469,6 +1497,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1483,8 +1512,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1315507081, + "version": 5, + "versionNonce": 406373543, "width": 10, "x": 0, "y": 0, @@ -1502,6 +1531,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1510,14 +1540,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1898319239, + "version": 5, + "versionNonce": 941653321, "width": 10, "x": 30, "y": 0, @@ -1534,6 +1564,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id7", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1542,14 +1573,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 81784553, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 640725609, + "version": 4, + "versionNonce": 908564423, "width": 10, "x": 60, "y": 0, @@ -1581,6 +1612,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1595,8 +1627,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1315507081, + "version": 5, + "versionNonce": 406373543, "width": 10, "x": 0, "y": 0, @@ -1614,6 +1646,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1622,14 +1655,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1898319239, + "version": 5, + "versionNonce": 941653321, "width": 10, "x": 30, "y": 0, @@ -1646,6 +1679,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id7", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1654,14 +1688,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 81784553, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 640725609, + "version": 4, + "versionNonce": 908564423, "width": 10, "x": 60, "y": 0, @@ -1693,6 +1727,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1707,8 +1742,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1315507081, + "version": 5, + "versionNonce": 406373543, "width": 10, "x": 0, "y": 0, @@ -1726,6 +1761,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -1734,14 +1770,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1898319239, + "version": 5, + "versionNonce": 941653321, "width": 10, "x": 30, "y": 0, @@ -1758,6 +1794,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin ], "height": 10, "id": "id7", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -1766,14 +1803,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 81784553, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 640725609, + "version": 4, + "versionNonce": 908564423, "width": 10, "x": 60, "y": 0, @@ -1931,6 +1968,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1945,8 +1983,8 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 401146281, + "version": 3, + "versionNonce": 2019559783, "width": 10, "x": 0, "y": 0, @@ -1975,6 +2013,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -1989,8 +2028,8 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 3, - "versionNonce": 1116226695, + "version": 4, + "versionNonce": 1014066025, "width": 10, "x": 25, "y": 25, @@ -2151,6 +2190,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2165,8 +2205,8 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -2195,6 +2235,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2209,8 +2250,8 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -2225,6 +2266,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2233,14 +2275,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -2269,6 +2311,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2283,8 +2326,8 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -2299,6 +2342,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2307,14 +2351,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -2329,6 +2373,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -2337,14 +2382,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 10, @@ -2376,6 +2421,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2384,14 +2430,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -2408,6 +2454,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor ], "height": 10, "id": "id0", + "index": "a1V", "isDeleted": false, "link": null, "locked": false, @@ -2422,8 +2469,8 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 5, + "versionNonce": 1006504105, "width": 10, "x": 10, "y": 10, @@ -2440,6 +2487,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor ], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -2448,14 +2496,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 760410951, "width": 10, "x": 50, "y": 10, @@ -2613,6 +2661,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2627,8 +2676,8 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -2657,6 +2706,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "groupIds": [], "height": 10, "id": "id0_copy", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2665,14 +2715,14 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 238820263, + "version": 5, + "versionNonce": 400692809, "width": 10, "x": 10, "y": 10, @@ -2687,6 +2737,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "groupIds": [], "height": 10, "id": "id0", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -2701,8 +2752,8 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1150084233, + "version": 5, + "versionNonce": 1604849351, "width": 10, "x": 20, "y": 20, @@ -2858,6 +2909,7 @@ exports[`regression tests > arrow keys > [end of test] history 1`] = ` "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -2872,8 +2924,8 @@ exports[`regression tests > arrow keys > [end of test] history 1`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -3031,6 +3083,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 200, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3045,8 +3098,8 @@ exports[`regression tests > can drag element that covers another element, while "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 200, "x": 100, "y": 100, @@ -3075,6 +3128,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 200, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3089,8 +3143,8 @@ exports[`regression tests > can drag element that covers another element, while "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 200, "x": 100, "y": 100, @@ -3105,6 +3159,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 200, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3113,14 +3168,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 200, "x": 100, "y": 100, @@ -3149,6 +3204,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 200, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3163,8 +3219,8 @@ exports[`regression tests > can drag element that covers another element, while "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 200, "x": 100, "y": 100, @@ -3179,6 +3235,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 200, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3187,14 +3244,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 200, "x": 100, "y": 100, @@ -3209,6 +3266,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 350, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -3217,14 +3275,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 350, "x": 300, "y": 300, @@ -3253,6 +3311,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 200, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3267,8 +3326,8 @@ exports[`regression tests > can drag element that covers another element, while "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 200, "x": 100, "y": 100, @@ -3283,6 +3342,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 200, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -3291,14 +3351,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 747212839, "width": 200, "x": 300, "y": 300, @@ -3313,6 +3373,7 @@ exports[`regression tests > can drag element that covers another element, while "groupIds": [], "height": 350, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -3321,14 +3382,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 350, "x": 300, "y": 300, @@ -3484,6 +3545,7 @@ exports[`regression tests > change the properties of a shape > [end of test] his "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3498,8 +3560,8 @@ exports[`regression tests > change the properties of a shape > [end of test] his "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -3528,6 +3590,7 @@ exports[`regression tests > change the properties of a shape > [end of test] his "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3542,8 +3605,8 @@ exports[`regression tests > change the properties of a shape > [end of test] his "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 2019559783, + "version": 4, + "versionNonce": 1150084233, "width": 10, "x": 10, "y": 10, @@ -3572,6 +3635,7 @@ exports[`regression tests > change the properties of a shape > [end of test] his "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3586,8 +3650,8 @@ exports[`regression tests > change the properties of a shape > [end of test] his "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1116226695, + "version": 5, + "versionNonce": 1014066025, "width": 10, "x": 10, "y": 10, @@ -3616,6 +3680,7 @@ exports[`regression tests > change the properties of a shape > [end of test] his "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3630,8 +3695,8 @@ exports[`regression tests > change the properties of a shape > [end of test] his "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, - "versionNonce": 238820263, + "version": 6, + "versionNonce": 400692809, "width": 10, "x": 10, "y": 10, @@ -3762,6 +3827,7 @@ exports[`regression tests > click on an element and drag it > [dragged] element "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3776,8 +3842,8 @@ exports[`regression tests > click on an element and drag it > [dragged] element "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1150084233, + "version": 4, + "versionNonce": 1116226695, "width": 10, "x": 20, "y": 20, @@ -3822,6 +3888,7 @@ exports[`regression tests > click on an element and drag it > [dragged] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3836,8 +3903,8 @@ exports[`regression tests > click on an element and drag it > [dragged] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -3866,6 +3933,7 @@ exports[`regression tests > click on an element and drag it > [dragged] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -3880,8 +3948,8 @@ exports[`regression tests > click on an element and drag it > [dragged] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1150084233, + "version": 4, + "versionNonce": 1116226695, "width": 10, "x": 20, "y": 20, @@ -4039,6 +4107,7 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4053,8 +4122,8 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -4083,6 +4152,7 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4097,8 +4167,8 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1150084233, + "version": 4, + "versionNonce": 1116226695, "width": 10, "x": 20, "y": 20, @@ -4127,6 +4197,7 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4141,8 +4212,8 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 400692809, + "version": 5, + "versionNonce": 1604849351, "width": 10, "x": 10, "y": 10, @@ -4300,6 +4371,7 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4314,8 +4386,8 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -4344,6 +4416,7 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4358,8 +4431,8 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -4374,6 +4447,7 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4382,14 +4456,14 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -4548,6 +4622,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4562,8 +4637,8 @@ exports[`regression tests > click-drag to select a group > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -4592,6 +4667,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4606,8 +4682,8 @@ exports[`regression tests > click-drag to select a group > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -4622,6 +4698,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4630,14 +4707,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -4666,6 +4743,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4680,8 +4758,8 @@ exports[`regression tests > click-drag to select a group > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -4696,6 +4774,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4704,14 +4783,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -4726,6 +4805,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -4734,14 +4814,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 10, @@ -4899,6 +4979,7 @@ exports[`regression tests > deleting last but one element in editing group shoul "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4913,8 +4994,8 @@ exports[`regression tests > deleting last but one element in editing group shoul "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 0, @@ -4943,6 +5024,7 @@ exports[`regression tests > deleting last but one element in editing group shoul "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -4957,8 +5039,8 @@ exports[`regression tests > deleting last but one element in editing group shoul "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 0, @@ -4973,6 +5055,7 @@ exports[`regression tests > deleting last but one element in editing group shoul "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -4981,14 +5064,14 @@ exports[`regression tests > deleting last but one element in editing group shoul "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 50, "y": 0, @@ -5022,6 +5105,7 @@ exports[`regression tests > deleting last but one element in editing group shoul ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5036,8 +5120,8 @@ exports[`regression tests > deleting last but one element in editing group shoul "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 0, @@ -5054,6 +5138,7 @@ exports[`regression tests > deleting last but one element in editing group shoul ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5062,14 +5147,14 @@ exports[`regression tests > deleting last but one element in editing group shoul "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 0, @@ -5100,6 +5185,7 @@ exports[`regression tests > deleting last but one element in editing group shoul ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": true, "link": null, "locked": false, @@ -5114,8 +5200,8 @@ exports[`regression tests > deleting last but one element in editing group shoul "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 493213705, + "version": 5, + "versionNonce": 81784553, "width": 10, "x": 10, "y": 0, @@ -5132,6 +5218,7 @@ exports[`regression tests > deleting last but one element in editing group shoul ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5140,14 +5227,14 @@ exports[`regression tests > deleting last but one element in editing group shoul "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 0, @@ -5180,6 +5267,7 @@ exports[`regression tests > deleting last but one element in editing group shoul ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": true, "link": null, "locked": false, @@ -5194,8 +5282,8 @@ exports[`regression tests > deleting last but one element in editing group shoul "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 493213705, + "version": 5, + "versionNonce": 81784553, "width": 10, "x": 10, "y": 0, @@ -5212,6 +5300,7 @@ exports[`regression tests > deleting last but one element in editing group shoul ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5220,14 +5309,14 @@ exports[`regression tests > deleting last but one element in editing group shoul "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 0, @@ -5279,6 +5368,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "groupIds": [], "height": 0, "id": "id3", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -5287,7 +5377,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 2, }, - "seed": 400692809, + "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5364,6 +5454,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "groupIds": [], "height": 0, "id": "id3", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -5372,7 +5463,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 2, }, - "seed": 400692809, + "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5442,6 +5533,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5456,8 +5548,8 @@ exports[`regression tests > deselects group of selected elements on pointer down "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -5486,6 +5578,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5500,8 +5593,8 @@ exports[`regression tests > deselects group of selected elements on pointer down "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -5516,6 +5609,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5524,14 +5618,14 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 110, "y": 110, @@ -5583,6 +5677,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "groupIds": [], "height": 0, "id": "id3", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -5591,7 +5686,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "roundness": { "type": 2, }, - "seed": 400692809, + "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5717,6 +5812,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5731,8 +5827,8 @@ exports[`regression tests > deselects group of selected elements on pointer up w "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -5761,6 +5857,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -5775,8 +5872,8 @@ exports[`regression tests > deselects group of selected elements on pointer up w "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -5791,6 +5888,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -5799,14 +5897,14 @@ exports[`regression tests > deselects group of selected elements on pointer up w "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 110, "y": 110, @@ -5858,6 +5956,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "groupIds": [], "height": 0, "id": "id1", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -5866,7 +5965,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5942,6 +6041,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "groupIds": [], "height": 0, "id": "id1", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -5950,7 +6050,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6020,6 +6120,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6034,8 +6135,8 @@ exports[`regression tests > deselects selected element on pointer down when poin "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -6191,6 +6292,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "groupIds": [], "height": 100, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6205,8 +6307,8 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 100, "x": 0, "y": 0, @@ -6362,6 +6464,7 @@ exports[`regression tests > double click to edit a group > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6376,8 +6479,8 @@ exports[`regression tests > double click to edit a group > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -6406,6 +6509,7 @@ exports[`regression tests > double click to edit a group > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6420,8 +6524,8 @@ exports[`regression tests > double click to edit a group > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -6436,6 +6540,7 @@ exports[`regression tests > double click to edit a group > [end of test] history "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -6444,14 +6549,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -6480,6 +6585,7 @@ exports[`regression tests > double click to edit a group > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6494,8 +6600,8 @@ exports[`regression tests > double click to edit a group > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -6510,6 +6616,7 @@ exports[`regression tests > double click to edit a group > [end of test] history "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -6518,14 +6625,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -6540,6 +6647,7 @@ exports[`regression tests > double click to edit a group > [end of test] history "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -6548,14 +6656,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 10, @@ -6590,6 +6698,7 @@ exports[`regression tests > double click to edit a group > [end of test] history ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6604,8 +6713,8 @@ exports[`regression tests > double click to edit a group > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 23633383, + "version": 4, + "versionNonce": 81784553, "width": 10, "x": 10, "y": 10, @@ -6622,6 +6731,7 @@ exports[`regression tests > double click to edit a group > [end of test] history ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -6630,14 +6740,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 747212839, "width": 10, "x": 30, "y": 10, @@ -6654,6 +6764,7 @@ exports[`regression tests > double click to edit a group > [end of test] history ], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -6662,14 +6773,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 1723083209, "width": 10, "x": 50, "y": 10, @@ -6829,6 +6940,7 @@ exports[`regression tests > drags selected elements from point inside common bou "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6843,8 +6955,8 @@ exports[`regression tests > drags selected elements from point inside common bou "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -6873,6 +6985,7 @@ exports[`regression tests > drags selected elements from point inside common bou "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6887,8 +7000,8 @@ exports[`regression tests > drags selected elements from point inside common bou "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -6903,6 +7016,7 @@ exports[`regression tests > drags selected elements from point inside common bou "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -6911,14 +7025,14 @@ exports[`regression tests > drags selected elements from point inside common bou "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 110, "y": 110, @@ -6948,6 +7062,7 @@ exports[`regression tests > drags selected elements from point inside common bou "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -6962,8 +7077,8 @@ exports[`regression tests > drags selected elements from point inside common bou "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 25, "y": 25, @@ -6978,6 +7093,7 @@ exports[`regression tests > drags selected elements from point inside common bou "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -6986,14 +7102,14 @@ exports[`regression tests > drags selected elements from point inside common bou "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 3, - "versionNonce": 23633383, + "version": 4, + "versionNonce": 915032327, "width": 10, "x": 135, "y": 135, @@ -7147,6 +7263,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7161,8 +7278,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -7191,6 +7308,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7205,8 +7323,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -7221,6 +7339,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7229,14 +7348,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -7265,6 +7384,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7279,8 +7399,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -7295,6 +7415,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7303,14 +7424,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -7325,6 +7446,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -7333,14 +7455,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -7369,6 +7491,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7383,8 +7506,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -7399,6 +7522,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7407,14 +7531,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -7429,6 +7553,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -7437,14 +7562,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -7461,6 +7586,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id3", + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -7480,7 +7606,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 23633383, + "seed": 81784553, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7488,8 +7614,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 1006504105, "width": 50, "x": 130, "y": -10, @@ -7518,6 +7644,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7532,8 +7659,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -7548,6 +7675,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7556,14 +7684,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -7578,6 +7706,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -7586,14 +7715,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -7610,6 +7739,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id3", + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -7629,7 +7759,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 23633383, + "seed": 81784553, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7637,8 +7767,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 1006504105, "width": 50, "x": 130, "y": -10, @@ -7655,6 +7785,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id4", + "index": "a4", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -7674,7 +7805,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1723083209, + "seed": 1315507081, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7682,8 +7813,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 289600103, + "version": 4, + "versionNonce": 941653321, "width": 50, "x": 220, "y": -10, @@ -7712,6 +7843,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7726,8 +7858,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -7742,6 +7874,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7750,14 +7883,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -7772,6 +7905,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -7780,14 +7914,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -7804,6 +7938,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id3", + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -7823,7 +7958,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 23633383, + "seed": 81784553, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7831,8 +7966,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 1006504105, "width": 50, "x": 130, "y": -10, @@ -7849,6 +7984,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id4", + "index": "a4", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -7868,7 +8004,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1723083209, + "seed": 1315507081, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7876,8 +8012,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 289600103, + "version": 4, + "versionNonce": 941653321, "width": 50, "x": 220, "y": -10, @@ -7894,6 +8030,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id5", + "index": "a5", "isDeleted": false, "lastCommittedPoint": [ 50, @@ -7916,7 +8053,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1898319239, + "seed": 1402203177, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7924,8 +8061,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 5, - "versionNonce": 1349943049, + "version": 6, + "versionNonce": 1163661225, "width": 50, "x": 310, "y": -10, @@ -7954,6 +8091,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -7968,8 +8106,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -7984,6 +8122,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7992,14 +8131,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -8014,6 +8153,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -8022,14 +8162,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -8046,6 +8186,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id3", + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8065,7 +8206,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 23633383, + "seed": 81784553, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8073,8 +8214,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 1006504105, "width": 50, "x": 130, "y": -10, @@ -8091,6 +8232,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id4", + "index": "a4", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8110,7 +8252,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1723083209, + "seed": 1315507081, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8118,8 +8260,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 289600103, + "version": 4, + "versionNonce": 941653321, "width": 50, "x": 220, "y": -10, @@ -8136,6 +8278,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 20, "id": "id5", + "index": "a5", "isDeleted": false, "lastCommittedPoint": [ 80, @@ -8162,7 +8305,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1898319239, + "seed": 1402203177, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8170,8 +8313,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 7, - "versionNonce": 1292308681, + "version": 8, + "versionNonce": 1996028265, "width": 80, "x": 310, "y": -10, @@ -8200,6 +8343,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -8214,8 +8358,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -8230,6 +8374,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -8238,14 +8383,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -8260,6 +8405,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -8268,14 +8414,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -8292,6 +8438,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id3", + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8311,7 +8458,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 23633383, + "seed": 81784553, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8319,8 +8466,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 1006504105, "width": 50, "x": 130, "y": -10, @@ -8337,6 +8484,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id4", + "index": "a4", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8356,7 +8504,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1723083209, + "seed": 1315507081, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8364,8 +8512,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 289600103, + "version": 4, + "versionNonce": 941653321, "width": 50, "x": 220, "y": -10, @@ -8382,6 +8530,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 20, "id": "id5", + "index": "a5", "isDeleted": false, "lastCommittedPoint": [ 80, @@ -8408,7 +8557,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1898319239, + "seed": 1402203177, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8416,8 +8565,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 7, - "versionNonce": 1292308681, + "version": 8, + "versionNonce": 1996028265, "width": 80, "x": 310, "y": -10, @@ -8434,6 +8583,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id6", + "index": "a6", "isDeleted": false, "lastCommittedPoint": [ 50, @@ -8456,7 +8606,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1508694887, + "seed": 1573789895, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8464,8 +8614,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 5, - "versionNonce": 1177973545, + "version": 6, + "versionNonce": 1189086535, "width": 50, "x": 430, "y": -10, @@ -8494,6 +8644,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -8508,8 +8659,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -8524,6 +8675,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -8532,14 +8684,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -8554,6 +8706,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -8562,14 +8715,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -8586,6 +8739,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id3", + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8605,7 +8759,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 23633383, + "seed": 81784553, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8613,8 +8767,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 1006504105, "width": 50, "x": 130, "y": -10, @@ -8631,6 +8785,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id4", + "index": "a4", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8650,7 +8805,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1723083209, + "seed": 1315507081, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8658,8 +8813,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 289600103, + "version": 4, + "versionNonce": 941653321, "width": 50, "x": 220, "y": -10, @@ -8676,6 +8831,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 20, "id": "id5", + "index": "a5", "isDeleted": false, "lastCommittedPoint": [ 80, @@ -8702,7 +8858,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1898319239, + "seed": 1402203177, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8710,8 +8866,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 7, - "versionNonce": 1292308681, + "version": 8, + "versionNonce": 1996028265, "width": 80, "x": 310, "y": -10, @@ -8728,6 +8884,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 20, "id": "id6", + "index": "a6", "isDeleted": false, "lastCommittedPoint": [ 80, @@ -8754,7 +8911,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1508694887, + "seed": 1573789895, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8762,8 +8919,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 7, - "versionNonce": 271613161, + "version": 8, + "versionNonce": 337026951, "width": 80, "x": 430, "y": -10, @@ -8790,6 +8947,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -8804,8 +8962,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -8820,6 +8978,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -8828,14 +8987,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 20, "x": 40, "y": -10, @@ -8850,6 +9009,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -8858,14 +9018,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 20, "x": 70, "y": -10, @@ -8882,6 +9042,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id3", + "index": "a3", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8901,7 +9062,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 23633383, + "seed": 81784553, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8909,8 +9070,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 1006504105, "width": 50, "x": 130, "y": -10, @@ -8927,6 +9088,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id4", + "index": "a4", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -8946,7 +9108,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1723083209, + "seed": 1315507081, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8954,8 +9116,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 289600103, + "version": 4, + "versionNonce": 941653321, "width": 50, "x": 220, "y": -10, @@ -8972,6 +9134,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 20, "id": "id5", + "index": "a5", "isDeleted": false, "lastCommittedPoint": [ 80, @@ -8998,7 +9161,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1898319239, + "seed": 1402203177, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -9006,8 +9169,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 7, - "versionNonce": 1292308681, + "version": 8, + "versionNonce": 1996028265, "width": 80, "x": 310, "y": -10, @@ -9024,6 +9187,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 20, "id": "id6", + "index": "a6", "isDeleted": false, "lastCommittedPoint": [ 80, @@ -9050,7 +9214,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1508694887, + "seed": 1573789895, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -9058,8 +9222,8 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 7, - "versionNonce": 271613161, + "version": 8, + "versionNonce": 337026951, "width": 80, "x": 430, "y": -10, @@ -9074,6 +9238,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id7", + "index": "a7", "isDeleted": false, "lastCommittedPoint": [ 50, @@ -9103,15 +9268,15 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] ], "roughness": 1, "roundness": null, - "seed": 1189086535, + "seed": 425216841, "simulatePressure": false, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "freedraw", "updated": 1, - "version": 4, - "versionNonce": 1439318121, + "version": 5, + "versionNonce": 1688617961, "width": 50, "x": 550, "y": -10, @@ -9270,6 +9435,7 @@ exports[`regression tests > given a group of selected elements with an element t "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -9284,8 +9450,8 @@ exports[`regression tests > given a group of selected elements with an element t "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -9314,6 +9480,7 @@ exports[`regression tests > given a group of selected elements with an element t "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -9328,8 +9495,8 @@ exports[`regression tests > given a group of selected elements with an element t "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -9344,6 +9511,7 @@ exports[`regression tests > given a group of selected elements with an element t "groupIds": [], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -9352,14 +9520,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 100, "x": 110, "y": 110, @@ -9388,6 +9556,7 @@ exports[`regression tests > given a group of selected elements with an element t "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -9402,8 +9571,8 @@ exports[`regression tests > given a group of selected elements with an element t "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -9418,6 +9587,7 @@ exports[`regression tests > given a group of selected elements with an element t "groupIds": [], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -9426,14 +9596,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 100, "x": 110, "y": 110, @@ -9448,6 +9618,7 @@ exports[`regression tests > given a group of selected elements with an element t "groupIds": [], "height": 100, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -9456,14 +9627,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 100, "x": 310, "y": 310, @@ -9622,6 +9793,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "groupIds": [], "height": 1000, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -9636,8 +9808,8 @@ exports[`regression tests > given a selected element A and a not selected elemen "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 401146281, + "version": 3, + "versionNonce": 2019559783, "width": 1000, "x": 0, "y": 0, @@ -9666,6 +9838,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "groupIds": [], "height": 1000, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -9680,8 +9853,8 @@ exports[`regression tests > given a selected element A and a not selected elemen "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 401146281, + "version": 3, + "versionNonce": 2019559783, "width": 1000, "x": 0, "y": 0, @@ -9696,6 +9869,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "groupIds": [], "height": 1000, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -9704,14 +9878,14 @@ exports[`regression tests > given a selected element A and a not selected elemen "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1014066025, + "version": 3, + "versionNonce": 400692809, "width": 1000, "x": 500, "y": 500, @@ -9869,6 +10043,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "groupIds": [], "height": 1000, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -9883,8 +10058,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 453191, "width": 1000, "x": 0, "y": 0, @@ -9899,6 +10074,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "groupIds": [], "height": 500, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -9913,8 +10089,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 401146281, "width": 500, "x": 500, "y": 500, @@ -10072,6 +10248,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "groupIds": [], "height": 1000, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -10086,8 +10263,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 453191, "width": 1000, "x": 0, "y": 0, @@ -10102,6 +10279,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "groupIds": [], "height": 500, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -10116,8 +10294,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 401146281, "width": 500, "x": 500, "y": 500, @@ -10146,6 +10324,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "groupIds": [], "height": 1000, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -10160,8 +10339,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1150084233, + "version": 3, + "versionNonce": 1014066025, "width": 1000, "x": 100, "y": 100, @@ -10176,6 +10355,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "groupIds": [], "height": 500, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -10190,8 +10370,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": 401146281, "width": 500, "x": 500, "y": 500, @@ -10347,6 +10527,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -10361,8 +10542,8 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -10518,6 +10699,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] history 1 "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -10532,8 +10714,8 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] history 1 "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -10689,6 +10871,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] history 1 "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -10703,8 +10886,8 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] history 1 "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -10885,6 +11068,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -10912,8 +11096,8 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 10, "x": 10, "y": 10, @@ -11094,6 +11278,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -11121,8 +11306,8 @@ exports[`regression tests > key 6 selects line tool > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 10, "x": 10, "y": 10, @@ -11274,6 +11459,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": [ 10, @@ -11310,8 +11496,8 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] history "strokeWidth": 2, "type": "freedraw", "updated": 1, - "version": 4, - "versionNonce": 1150084233, + "version": 5, + "versionNonce": 1116226695, "width": 10, "x": 10, "y": 10, @@ -11492,6 +11678,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -11519,8 +11706,8 @@ exports[`regression tests > key a selects arrow tool > [end of test] history 1`] "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 10, "x": 10, "y": 10, @@ -11676,6 +11863,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] history 1 "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -11690,8 +11878,8 @@ exports[`regression tests > key d selects diamond tool > [end of test] history 1 "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -11872,6 +12060,7 @@ exports[`regression tests > key l selects line tool > [end of test] history 1`] "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -11899,8 +12088,8 @@ exports[`regression tests > key l selects line tool > [end of test] history 1`] "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 10, "x": 10, "y": 10, @@ -12056,6 +12245,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] history 1 "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -12070,8 +12260,8 @@ exports[`regression tests > key o selects ellipse tool > [end of test] history 1 "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -12223,6 +12413,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": [ 10, @@ -12259,8 +12450,8 @@ exports[`regression tests > key p selects freedraw tool > [end of test] history "strokeWidth": 2, "type": "freedraw", "updated": 1, - "version": 4, - "versionNonce": 1150084233, + "version": 5, + "versionNonce": 1116226695, "width": 10, "x": 10, "y": 10, @@ -12416,6 +12607,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -12430,8 +12622,8 @@ exports[`regression tests > key r selects rectangle tool > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -12595,6 +12787,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -12609,8 +12802,8 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -12639,6 +12832,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -12653,8 +12847,8 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -12669,6 +12863,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -12677,14 +12872,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -12713,6 +12908,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -12727,8 +12923,8 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -12743,6 +12939,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -12751,14 +12948,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -12773,6 +12970,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -12781,14 +12979,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 10, @@ -12823,6 +13021,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -12837,8 +13036,8 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 1723083209, "width": 10, "x": 10, "y": 10, @@ -12855,6 +13054,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -12863,14 +13063,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 760410951, "width": 10, "x": 30, "y": 10, @@ -12887,6 +13087,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -12895,14 +13096,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 747212839, + "version": 4, + "versionNonce": 1006504105, "width": 10, "x": 50, "y": 10, @@ -12937,6 +13138,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id0_copy", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -12945,14 +13147,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 941653321, + "seed": 1359939303, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, - "versionNonce": 908564423, + "version": 6, + "versionNonce": 1349943049, "width": 10, "x": 10, "y": 10, @@ -12969,6 +13171,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id1_copy", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -12977,14 +13180,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 1402203177, + "seed": 2004587015, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, - "versionNonce": 1359939303, + "version": 6, + "versionNonce": 2101589481, "width": 10, "x": 30, "y": 10, @@ -13001,6 +13204,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id2_copy", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -13009,14 +13213,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 1349943049, + "seed": 845789479, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, - "versionNonce": 2004587015, + "version": 6, + "versionNonce": 1292308681, "width": 10, "x": 50, "y": 10, @@ -13033,6 +13237,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id0", + "index": "a3", "isDeleted": false, "link": null, "locked": false, @@ -13047,8 +13252,8 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1006504105, + "version": 6, + "versionNonce": 927333447, "width": 10, "x": 20, "y": 20, @@ -13065,6 +13270,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id1", + "index": "a4", "isDeleted": false, "link": null, "locked": false, @@ -13073,14 +13279,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1315507081, + "version": 6, + "versionNonce": 1163661225, "width": 10, "x": 40, "y": 20, @@ -13097,6 +13303,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor ], "height": 10, "id": "id2", + "index": "a5", "isDeleted": false, "link": null, "locked": false, @@ -13105,14 +13312,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 640725609, + "version": 6, + "versionNonce": 1508694887, "width": 10, "x": 60, "y": 20, @@ -13270,6 +13477,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -13284,8 +13492,8 @@ exports[`regression tests > noop interaction after undo shouldn't create history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -13314,6 +13522,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -13328,8 +13537,8 @@ exports[`regression tests > noop interaction after undo shouldn't create history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -13344,6 +13553,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -13352,14 +13562,14 @@ exports[`regression tests > noop interaction after undo shouldn't create history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -13640,6 +13850,7 @@ exports[`regression tests > shift click on selected element should deselect it o "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -13654,8 +13865,8 @@ exports[`regression tests > shift click on selected element should deselect it o "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -13815,6 +14026,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -13829,8 +14041,8 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -13859,6 +14071,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -13873,8 +14086,8 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -13889,6 +14102,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -13897,14 +14111,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -13934,6 +14148,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -13948,8 +14163,8 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 20, "y": 20, @@ -13964,6 +14179,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -13972,14 +14188,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 81784553, "width": 10, "x": 40, "y": 20, @@ -14141,6 +14357,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14155,8 +14372,8 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -14185,6 +14402,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14199,8 +14417,8 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -14215,6 +14433,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -14223,14 +14442,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -14259,6 +14478,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14273,8 +14493,8 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -14289,6 +14509,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -14297,14 +14518,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 30, "y": 10, @@ -14319,6 +14540,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -14327,14 +14549,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 10, @@ -14369,6 +14591,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14383,8 +14606,8 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 1723083209, "width": 10, "x": 10, "y": 10, @@ -14401,6 +14624,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -14409,14 +14633,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 81784553, + "version": 4, + "versionNonce": 760410951, "width": 10, "x": 30, "y": 10, @@ -14433,6 +14657,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes ], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -14441,14 +14666,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 747212839, + "version": 4, + "versionNonce": 1006504105, "width": 10, "x": 50, "y": 10, @@ -14479,6 +14704,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14492,9 +14718,9 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", - "updated": 1, - "version": 4, - "versionNonce": 289600103, + "updated": 1, + "version": 5, + "versionNonce": 640725609, "width": 10, "x": 10, "y": 10, @@ -14509,6 +14735,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -14517,14 +14744,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1315507081, + "version": 5, + "versionNonce": 406373543, "width": 10, "x": 30, "y": 10, @@ -14539,6 +14766,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -14547,14 +14775,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1898319239, + "version": 5, + "versionNonce": 941653321, "width": 10, "x": 50, "y": 10, @@ -14720,6 +14948,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14734,8 +14963,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 0, @@ -14764,6 +14993,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14778,8 +15008,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 0, @@ -14794,6 +15024,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -14802,14 +15033,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 50, "y": 0, @@ -14843,6 +15074,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14857,8 +15089,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 0, @@ -14875,6 +15107,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -14883,14 +15116,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 0, @@ -14921,6 +15154,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -14935,8 +15169,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 0, @@ -14953,6 +15187,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -14961,14 +15196,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 0, @@ -14983,6 +15218,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [], "height": 10, "id": "id5", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -14991,14 +15227,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 493213705, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 81784553, + "version": 3, + "versionNonce": 760410951, "width": 10, "x": 10, "y": 50, @@ -15029,6 +15265,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -15043,8 +15280,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 0, @@ -15061,6 +15298,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -15069,14 +15307,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 0, @@ -15091,6 +15329,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [], "height": 10, "id": "id5", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -15099,14 +15338,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 493213705, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 81784553, + "version": 3, + "versionNonce": 760410951, "width": 10, "x": 10, "y": 50, @@ -15121,6 +15360,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [], "height": 10, "id": "id6", + "index": "a3", "isDeleted": false, "link": null, "locked": false, @@ -15129,14 +15369,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 1723083209, + "seed": 289600103, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1006504105, + "version": 3, + "versionNonce": 640725609, "width": 10, "x": 50, "y": 50, @@ -15170,6 +15410,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -15184,8 +15425,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1604849351, + "version": 4, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 0, @@ -15202,6 +15443,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -15210,14 +15452,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 1505387817, + "version": 4, + "versionNonce": 493213705, "width": 10, "x": 50, "y": 0, @@ -15234,6 +15476,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id5", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -15242,14 +15485,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 493213705, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 640725609, + "version": 4, + "versionNonce": 1402203177, "width": 10, "x": 10, "y": 50, @@ -15266,6 +15509,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id6", + "index": "a3", "isDeleted": false, "link": null, "locked": false, @@ -15274,14 +15518,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 1723083209, + "seed": 289600103, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 406373543, + "version": 4, + "versionNonce": 1359939303, "width": 10, "x": 50, "y": 50, @@ -15318,6 +15562,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -15332,8 +15577,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 908564423, + "version": 5, + "versionNonce": 2004587015, "width": 10, "x": 10, "y": 0, @@ -15351,6 +15596,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -15359,14 +15605,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1402203177, + "version": 5, + "versionNonce": 2101589481, "width": 10, "x": 50, "y": 0, @@ -15384,6 +15630,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id5", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -15392,14 +15639,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 493213705, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1359939303, + "version": 5, + "versionNonce": 845789479, "width": 10, "x": 10, "y": 50, @@ -15417,6 +15664,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh ], "height": 10, "id": "id6", + "index": "a3", "isDeleted": false, "link": null, "locked": false, @@ -15425,14 +15673,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 1723083209, + "seed": 289600103, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1349943049, + "version": 5, + "versionNonce": 1292308681, "width": 10, "x": 50, "y": 50, @@ -15718,6 +15966,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -15732,8 +15981,8 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 50, "x": 0, "y": 0, @@ -15762,6 +16011,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -15776,8 +16026,8 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 50, "x": 0, "y": 0, @@ -15792,6 +16042,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "groupIds": [], "height": 50, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -15800,14 +16051,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 50, "x": 100, "y": 100, @@ -15836,6 +16087,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -15850,8 +16102,8 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 50, "x": 0, "y": 0, @@ -15866,6 +16118,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "groupIds": [], "height": 50, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -15874,14 +16127,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 50, "x": 100, "y": 100, @@ -15896,6 +16149,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "groupIds": [], "height": 50, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -15904,14 +16158,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1604849351, + "version": 3, + "versionNonce": 493213705, "width": 50, "x": 200, "y": 200, @@ -15946,6 +16200,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -15960,8 +16215,8 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 23633383, + "version": 4, + "versionNonce": 81784553, "width": 50, "x": 0, "y": 0, @@ -15978,6 +16233,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -15986,14 +16242,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 747212839, "width": 50, "x": 100, "y": 100, @@ -16010,6 +16266,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -16018,14 +16275,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 1723083209, "width": 50, "x": 200, "y": 200, @@ -16057,6 +16314,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -16071,8 +16329,8 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 23633383, + "version": 4, + "versionNonce": 81784553, "width": 50, "x": 0, "y": 0, @@ -16089,6 +16347,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -16097,14 +16356,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 747212839, "width": 50, "x": 100, "y": 100, @@ -16121,6 +16380,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -16129,14 +16389,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 915032327, + "version": 4, + "versionNonce": 1723083209, "width": 50, "x": 200, "y": 200, @@ -16170,6 +16430,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -16178,14 +16439,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 747212839, "width": 50, "x": 100, "y": 100, @@ -16203,6 +16464,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id0", + "index": "a1V", "isDeleted": false, "link": null, "locked": false, @@ -16217,8 +16479,8 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1723083209, + "version": 6, + "versionNonce": 1898319239, "width": 50, "x": 0, "y": 0, @@ -16236,6 +16498,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -16244,14 +16507,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 760410951, + "version": 5, + "versionNonce": 1315507081, "width": 50, "x": 200, "y": 200, @@ -16286,6 +16549,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -16294,14 +16558,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 3, - "versionNonce": 493213705, + "version": 4, + "versionNonce": 747212839, "width": 50, "x": 100, "y": 100, @@ -16319,6 +16583,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id0", + "index": "a1V", "isDeleted": false, "link": null, "locked": false, @@ -16333,8 +16598,8 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 1723083209, + "version": 6, + "versionNonce": 1898319239, "width": 50, "x": 0, "y": 0, @@ -16352,6 +16617,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = ], "height": 50, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -16360,14 +16626,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, - "versionNonce": 760410951, + "version": 5, + "versionNonce": 1315507081, "width": 50, "x": 200, "y": 200, @@ -16419,6 +16685,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 0, "id": "id4", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -16427,7 +16694,7 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 915032327, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16506,6 +16773,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 0, "id": "id4", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -16514,7 +16782,7 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 915032327, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16584,6 +16852,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -16598,8 +16867,8 @@ exports[`regression tests > switches from group of selected elements to another "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 401146281, + "version": 3, + "versionNonce": 2019559783, "width": 10, "x": 0, "y": 0, @@ -16628,6 +16897,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -16642,8 +16912,8 @@ exports[`regression tests > switches from group of selected elements to another "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 401146281, + "version": 3, + "versionNonce": 2019559783, "width": 10, "x": 0, "y": 0, @@ -16658,6 +16928,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -16666,14 +16937,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1014066025, + "version": 3, + "versionNonce": 400692809, "width": 100, "x": 110, "y": 110, @@ -16702,6 +16973,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -16716,8 +16988,8 @@ exports[`regression tests > switches from group of selected elements to another "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 401146281, + "version": 3, + "versionNonce": 2019559783, "width": 10, "x": 0, "y": 0, @@ -16732,6 +17004,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 100, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -16740,14 +17013,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1014066025, + "version": 3, + "versionNonce": 400692809, "width": 100, "x": 110, "y": 110, @@ -16762,6 +17035,7 @@ exports[`regression tests > switches from group of selected elements to another "groupIds": [], "height": 100, "id": "id2", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -16770,14 +17044,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 400692809, + "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 1505387817, + "version": 3, + "versionNonce": 915032327, "width": 100, "x": 310, "y": 310, @@ -16829,6 +17103,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "groupIds": [], "height": 0, "id": "id2", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -16837,7 +17112,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16915,6 +17190,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "groupIds": [], "height": 0, "id": "id2", + "index": null, "isDeleted": false, "link": null, "locked": false, @@ -16923,7 +17199,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16993,6 +17269,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -17007,8 +17284,8 @@ exports[`regression tests > switches selected element on pointer down > [end of "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -17037,6 +17314,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -17051,8 +17329,8 @@ exports[`regression tests > switches selected element on pointer down > [end of "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 10, "x": 0, "y": 0, @@ -17067,6 +17345,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "groupIds": [], "height": 10, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -17075,14 +17354,14 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 10, "x": 20, "y": 20, @@ -17351,6 +17630,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -17365,8 +17645,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -17381,6 +17661,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -17389,14 +17670,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 30, "x": 40, "y": 0, @@ -17413,6 +17694,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 20, "id": "id2", + "index": "a2", "isDeleted": false, "lastCommittedPoint": [ 100, @@ -17439,7 +17721,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -17447,8 +17729,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 7, - "versionNonce": 1006504105, + "version": 8, + "versionNonce": 1898319239, "width": 100, "x": 130, "y": 10, @@ -17477,6 +17759,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -17491,8 +17774,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -17507,6 +17790,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -17515,14 +17799,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 30, "x": 40, "y": 0, @@ -17539,6 +17823,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 10, "id": "id2", + "index": "a2", "isDeleted": false, "lastCommittedPoint": [ 60, @@ -17561,7 +17846,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1604849351, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -17569,8 +17854,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 5, - "versionNonce": 81784553, + "version": 6, + "versionNonce": 760410951, "width": 60, "x": 130, "y": 10, @@ -17612,6 +17897,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -17626,8 +17912,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -17656,6 +17942,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 10, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -17670,8 +17957,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 20, "x": 10, "y": -10, @@ -17686,6 +17973,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "groupIds": [], "height": 20, "id": "id1", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -17694,14 +17982,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 2019559783, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 1116226695, + "version": 3, + "versionNonce": 238820263, "width": 30, "x": 40, "y": 0, diff --git a/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap index 9b0ebecc620ca..42a3aa84e506c 100644 --- a/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap @@ -13,6 +13,7 @@ exports[`select single element on the scene > arrow 1`] = ` "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -40,8 +41,8 @@ exports[`select single element on the scene > arrow 1`] = ` "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 30, "x": 10, "y": 10, @@ -61,6 +62,7 @@ exports[`select single element on the scene > arrow escape 1`] = ` "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -88,8 +90,8 @@ exports[`select single element on the scene > arrow escape 1`] = ` "strokeWidth": 2, "type": "line", "updated": 1, - "version": 3, - "versionNonce": 401146281, + "version": 4, + "versionNonce": 2019559783, "width": 30, "x": 10, "y": 10, @@ -107,6 +109,7 @@ exports[`select single element on the scene > diamond 1`] = ` "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -121,8 +124,8 @@ exports[`select single element on the scene > diamond 1`] = ` "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 30, "x": 10, "y": 10, @@ -140,6 +143,7 @@ exports[`select single element on the scene > ellipse 1`] = ` "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -154,8 +158,8 @@ exports[`select single element on the scene > ellipse 1`] = ` "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 30, "x": 10, "y": 10, @@ -173,6 +177,7 @@ exports[`select single element on the scene > rectangle 1`] = ` "groupIds": [], "height": 50, "id": "id0", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -187,8 +192,8 @@ exports[`select single element on the scene > rectangle 1`] = ` "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 2, - "versionNonce": 453191, + "version": 3, + "versionNonce": 401146281, "width": 30, "x": 10, "y": 10, diff --git a/packages/excalidraw/tests/contextmenu.test.tsx b/packages/excalidraw/tests/contextmenu.test.tsx index f034dbd8c246f..643cedcfbefb2 100644 --- a/packages/excalidraw/tests/contextmenu.test.tsx +++ b/packages/excalidraw/tests/contextmenu.test.tsx @@ -423,8 +423,26 @@ describe("contextMenu element", () => { const contextMenu = UI.queryContextMenu(); fireEvent.click(queryByText(contextMenu!, "Duplicate")!); expect(h.elements).toHaveLength(2); - const { id: _id0, seed: _seed0, x: _x0, y: _y0, ...rect1 } = h.elements[0]; - const { id: _id1, seed: _seed1, x: _x1, y: _y1, ...rect2 } = h.elements[1]; + const { + id: _id0, + seed: _seed0, + x: _x0, + y: _y0, + index: _fractionalIndex0, + version: _version0, + versionNonce: _versionNonce0, + ...rect1 + } = h.elements[0]; + const { + id: _id1, + seed: _seed1, + x: _x1, + y: _y1, + index: _fractionalIndex1, + version: _version1, + versionNonce: _versionNonce1, + ...rect2 + } = h.elements[1]; expect(rect1).toEqual(rect2); }); diff --git a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap index 156e839a38b61..8921653f51d63 100644 --- a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap +++ b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap @@ -13,6 +13,7 @@ exports[`restoreElements > should restore arrow element correctly 1`] = ` "groupIds": [], "height": 100, "id": "id-arrow01", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -40,8 +41,8 @@ exports[`restoreElements > should restore arrow element correctly 1`] = ` "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "width": 100, "x": 0, "y": 0, @@ -63,6 +64,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and ], "height": 200, "id": "1", + "index": "a0", "isDeleted": false, "link": null, "locked": false, @@ -77,8 +79,8 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "width": 100, "x": 10, "y": 20, @@ -100,6 +102,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and ], "height": 200, "id": "2", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -114,8 +117,8 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and "strokeWidth": 2, "type": "ellipse", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "width": 100, "x": 10, "y": 20, @@ -137,6 +140,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and ], "height": 200, "id": "3", + "index": "a2", "isDeleted": false, "link": null, "locked": false, @@ -151,8 +155,8 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and "strokeWidth": 2, "type": "diamond", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "width": 100, "x": 10, "y": 20, @@ -170,6 +174,7 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = ` "groupIds": [], "height": 0, "id": "id-freedraw01", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -188,8 +193,8 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = ` "strokeWidth": 2, "type": "freedraw", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "width": 0, "x": 0, "y": 0, @@ -209,6 +214,7 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] = "groupIds": [], "height": 100, "id": "id-line01", + "index": "a0", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -236,8 +242,8 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] = "strokeWidth": 2, "type": "line", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "width": 100, "x": 0, "y": 0, @@ -257,6 +263,7 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] = "groupIds": [], "height": 100, "id": "id-draw01", + "index": "a1", "isDeleted": false, "lastCommittedPoint": null, "link": null, @@ -284,8 +291,8 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] = "strokeWidth": 2, "type": "line", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "width": 100, "x": 0, "y": 0, @@ -306,6 +313,7 @@ exports[`restoreElements > should restore text element correctly passing value f "groupIds": [], "height": 100, "id": "id-text01", + "index": "a0", "isDeleted": false, "lineHeight": 1.25, "link": null, @@ -324,8 +332,8 @@ exports[`restoreElements > should restore text element correctly passing value f "textAlign": "center", "type": "text", "updated": 1, - "version": 1, - "versionNonce": 0, + "version": 2, + "versionNonce": Any, "verticalAlign": "middle", "width": 100, "x": -20, @@ -347,6 +355,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo "groupIds": [], "height": 100, "id": "id-text01", + "index": "a0", "isDeleted": true, "lineHeight": 1.25, "link": null, @@ -365,7 +374,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo "textAlign": "left", "type": "text", "updated": 1, - "version": 2, + "version": 3, "versionNonce": Any, "verticalAlign": "top", "width": 100, diff --git a/packages/excalidraw/tests/data/reconcile.test.ts b/packages/excalidraw/tests/data/reconcile.test.ts new file mode 100644 index 0000000000000..a6912699fdb8e --- /dev/null +++ b/packages/excalidraw/tests/data/reconcile.test.ts @@ -0,0 +1,374 @@ +import { + RemoteExcalidrawElement, + reconcileElements, +} from "../../data/reconcile"; +import { + ExcalidrawElement, + OrderedExcalidrawElement, +} from "../../element/types"; +import { syncInvalidIndices } from "../../fractionalIndex"; +import { randomInteger } from "../../random"; +import { AppState } from "../../types"; +import { cloneJSON } from "../../utils"; + +type Id = string; +type ElementLike = { + id: string; + version: number; + versionNonce: number; + index: string; +}; + +type Cache = Record; + +const createElement = (opts: { uid: string } | ElementLike) => { + let uid: string; + let id: string; + let version: number | null; + let versionNonce: number | null = null; + if ("uid" in opts) { + const match = opts.uid.match(/^(\w+)(?::(\d+))?$/)!; + id = match[1]; + version = match[2] ? parseInt(match[2]) : null; + uid = version ? `${id}:${version}` : id; + } else { + ({ id, version, versionNonce } = opts); + uid = id; + } + return { + uid, + id, + version, + versionNonce: versionNonce || randomInteger(), + }; +}; + +const idsToElements = (ids: (Id | ElementLike)[], cache: Cache = {}) => { + return syncInvalidIndices( + ids.reduce((acc, _uid) => { + const { uid, id, version, versionNonce } = createElement( + typeof _uid === "string" ? { uid: _uid } : _uid, + ); + const cached = cache[uid]; + const elem = { + id, + version: version ?? 0, + versionNonce, + ...cached, + } as ExcalidrawElement; + // @ts-ignore + cache[uid] = elem; + acc.push(elem); + return acc; + }, [] as ExcalidrawElement[]), + ); +}; + +const test = ( + local: (Id | ElementLike)[], + remote: (Id | ElementLike)[], + target: U[], +) => { + const cache: Cache = {}; + const _local = idsToElements(local, cache); + const _remote = idsToElements(remote, cache); + + const reconciled = reconcileElements( + cloneJSON(_local), + cloneJSON(_remote) as RemoteExcalidrawElement[], + {} as AppState, + ); + + const reconciledIds = reconciled.map((x) => x.id); + const reconciledIndices = reconciled.map((x) => x.index); + + expect(target.length).equal(reconciled.length); + expect(reconciledIndices.length).equal(new Set([...reconciledIndices]).size); // expect no duplicated indices + expect(reconciledIds).deep.equal( + target.map((uid) => { + const [, id, source] = uid.match(/^(\w+):([LR])$/)!; + const element = (source === "L" ? _local : _remote).find( + (e) => e.id === id, + )!; + + return element.id; + }), + "remote reconciliation", + ); + + // convergent reconciliation on the remote client + try { + expect( + reconcileElements( + cloneJSON(_remote), + cloneJSON(_local as RemoteExcalidrawElement[]), + {} as AppState, + ).map((x) => x.id), + ).deep.equal(reconciledIds, "convergent reconciliation"); + } catch (error: any) { + console.error("local original", _remote); + console.error("remote original", _local); + throw error; + } + + // bidirectional re-reconciliation on remote client + try { + expect( + reconcileElements( + cloneJSON(_remote), + cloneJSON(reconciled as unknown as RemoteExcalidrawElement[]), + {} as AppState, + ).map((x) => x.id), + ).deep.equal(reconciledIds, "local re-reconciliation"); + } catch (error: any) { + console.error("local original", _remote); + console.error("remote reconciled", reconciled); + throw error; + } +}; + +describe("elements reconciliation", () => { + it("reconcileElements()", () => { + // ------------------------------------------------------------------------- + // + // in following tests, we pass: + // (1) an array of local elements and their version (:1, :2...) + // (2) an array of remote elements and their version (:1, :2...) + // (3) expected reconciled elements + // + // in the reconciled array: + // :L means local element was resolved + // :R means remote element was resolved + // + // if versions are missing, it defaults to version 0 + // ------------------------------------------------------------------------- + + test(["A:1", "B:1", "C:1"], ["B:2"], ["A:L", "B:R", "C:L"]); + test(["A:1", "B:1", "C"], ["B:2", "A:2"], ["B:R", "A:R", "C:L"]); + test(["A:2", "B:1", "C"], ["B:2", "A:1"], ["A:L", "B:R", "C:L"]); + test(["A:1", "C:1"], ["B:1"], ["A:L", "B:R", "C:L"]); + test(["A", "B"], ["A:1"], ["A:R", "B:L"]); + test(["A"], ["A", "B"], ["A:L", "B:R"]); + test(["A"], ["A:1", "B"], ["A:R", "B:R"]); + test(["A:2"], ["A:1", "B"], ["A:L", "B:R"]); + test(["A:2"], ["B", "A:1"], ["A:L", "B:R"]); + test(["A:1"], ["B", "A:2"], ["B:R", "A:R"]); + test(["A"], ["A:1"], ["A:R"]); + test(["A", "B:1", "D"], ["B", "C:2", "A"], ["C:R", "A:R", "B:L", "D:L"]); + + // some of the following tests are kinda arbitrary and they're less + // likely to happen in real-world cases + test(["A", "B"], ["B:1", "A:1"], ["B:R", "A:R"]); + test(["A:2", "B:2"], ["B:1", "A:1"], ["A:L", "B:L"]); + test(["A", "B", "C"], ["A", "B:2", "G", "C"], ["A:L", "B:R", "G:R", "C:L"]); + test(["A", "B", "C"], ["A", "B:2", "G"], ["A:R", "B:R", "C:L", "G:R"]); + test( + ["A:2", "B:2", "C"], + ["D", "B:1", "A:3"], + ["D:R", "B:L", "A:R", "C:L"], + ); + test( + ["A:2", "B:2", "C"], + ["D", "B:2", "A:3", "C"], + ["D:R", "B:L", "A:R", "C:L"], + ); + test( + ["A", "B", "C", "D", "E", "F"], + ["A", "B:2", "X", "E:2", "F", "Y"], + ["A:L", "B:R", "X:R", "C:L", "E:R", "D:L", "F:L", "Y:R"], + ); + + // fractional elements (previously annotated) + test( + ["A", "B", "C"], + ["A", "B", "X", "Y", "Z"], + ["A:R", "B:R", "C:L", "X:R", "Y:R", "Z:R"], + ); + + test(["A"], ["X", "Y"], ["A:L", "X:R", "Y:R"]); + test(["A"], ["X", "Y", "Z"], ["A:L", "X:R", "Y:R", "Z:R"]); + test(["A", "B"], ["C", "D", "F"], ["A:L", "C:R", "B:L", "D:R", "F:R"]); + + test( + ["A", "B", "C", "D"], + ["C:1", "B", "D:1"], + ["A:L", "C:R", "B:L", "D:R"], + ); + test( + ["A", "B", "C"], + ["X", "A", "Y", "B", "Z"], + ["X:R", "A:R", "Y:R", "B:L", "C:L", "Z:R"], + ); + test( + ["B", "A", "C"], + ["X", "A", "Y", "B", "Z"], + ["X:R", "A:R", "C:L", "Y:R", "B:R", "Z:R"], + ); + test(["A", "B"], ["A", "X", "Y"], ["A:R", "B:L", "X:R", "Y:R"]); + test( + ["A", "B", "C", "D", "E"], + ["A", "X", "C", "Y", "D", "Z"], + ["A:R", "B:L", "X:R", "C:R", "Y:R", "D:R", "E:L", "Z:R"], + ); + test( + ["X", "Y", "Z"], + ["A", "B", "C"], + ["A:R", "X:L", "B:R", "Y:L", "C:R", "Z:L"], + ); + test( + ["X", "Y", "Z"], + ["A", "B", "C", "X", "D", "Y", "Z"], + ["A:R", "B:R", "C:R", "X:L", "D:R", "Y:L", "Z:L"], + ); + test( + ["A", "B", "C", "D", "E"], + ["C", "X", "A", "Y", "D", "E:1"], + ["B:L", "C:L", "X:R", "A:R", "Y:R", "D:R", "E:R"], + ); + test( + ["C:1", "B", "D:1"], + ["A", "B", "C:1", "D:1"], + ["A:R", "B:R", "C:R", "D:R"], + ); + + test( + ["C:1", "B", "D:1"], + ["A", "B", "C:2", "D:1"], + ["A:R", "B:L", "C:R", "D:L"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "C:1", "B", "D:1"], + ["A:L", "C:R", "B:L", "D:R"], + ); + + test( + ["A", "B", "C", "D"], + ["C", "X", "B", "Y", "A", "Z"], + ["C:R", "D:L", "X:R", "B:R", "Y:R", "A:R", "Z:R"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "B:1", "C:1"], + ["A:R", "B:R", "C:R", "D:L"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "C:1", "B:1"], + ["A:R", "C:R", "B:R", "D:L"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "C:1", "B", "D:1"], + ["A:R", "C:R", "B:R", "D:R"], + ); + + test(["A:1", "B:1", "C"], ["B:2"], ["A:L", "B:R", "C:L"]); + test(["A:1", "B:1", "C"], ["B:2", "C:2"], ["A:L", "B:R", "C:R"]); + test(["A", "B"], ["A", "C", "B", "D"], ["A:R", "C:R", "B:R", "D:R"]); + test(["A", "B"], ["B", "C", "D"], ["A:L", "B:R", "C:R", "D:R"]); + test(["A", "B"], ["C", "D"], ["A:L", "C:R", "B:L", "D:R"]); + test(["A", "B"], ["A", "B:1"], ["A:L", "B:R"]); + test(["A:2", "B"], ["A", "B:1"], ["A:L", "B:R"]); + test(["A:2", "B:2"], ["B:1"], ["A:L", "B:L"]); + test(["A:2", "B:2"], ["B:1", "C"], ["A:L", "B:L", "C:R"]); + test(["A:2", "B:2"], ["A", "C", "B:1"], ["A:L", "B:L", "C:R"]); + + // concurrent convergency + test(["A", "B", "C"], ["A", "B", "D"], ["A:R", "B:R", "C:L", "D:R"]); + test(["A", "B", "E"], ["A", "B", "D"], ["A:R", "B:R", "D:R", "E:L"]); + test( + ["A", "B", "C"], + ["A", "B", "D", "E"], + ["A:R", "B:R", "C:L", "D:R", "E:R"], + ); + test( + ["A", "B", "E"], + ["A", "B", "D", "C"], + ["A:R", "B:R", "D:R", "E:L", "C:R"], + ); + test(["A", "B"], ["B", "D"], ["A:L", "B:R", "D:R"]); + test(["C", "A", "B"], ["C", "B", "D"], ["C:R", "A:L", "B:R", "D:R"]); + }); + + it("test identical elements reconciliation", () => { + const testIdentical = ( + local: ElementLike[], + remote: ElementLike[], + expected: Id[], + ) => { + const ret = reconcileElements( + local as unknown as OrderedExcalidrawElement[], + remote as unknown as RemoteExcalidrawElement[], + {} as AppState, + ); + + if (new Set(ret.map((x) => x.id)).size !== ret.length) { + throw new Error("reconcileElements: duplicate elements found"); + } + + expect(ret.map((x) => x.id)).to.deep.equal(expected); + }; + + // identical id/version/versionNonce/index + // ------------------------------------------------------------------------- + + testIdentical( + [{ id: "A", version: 1, versionNonce: 1, index: "a0" }], + [{ id: "A", version: 1, versionNonce: 1, index: "a0" }], + ["A"], + ); + testIdentical( + [ + { id: "A", version: 1, versionNonce: 1, index: "a0" }, + { id: "B", version: 1, versionNonce: 1, index: "a0" }, + ], + [ + { id: "B", version: 1, versionNonce: 1, index: "a0" }, + { id: "A", version: 1, versionNonce: 1, index: "a0" }, + ], + ["A", "B"], + ); + + // actually identical (arrays and element objects) + // ------------------------------------------------------------------------- + + const elements1 = [ + { + id: "A", + version: 1, + versionNonce: 1, + index: "a0", + }, + { + id: "B", + version: 1, + versionNonce: 1, + index: "a0", + }, + ]; + + testIdentical(elements1, elements1, ["A", "B"]); + testIdentical(elements1, elements1.slice(), ["A", "B"]); + testIdentical(elements1.slice(), elements1, ["A", "B"]); + testIdentical(elements1.slice(), elements1.slice(), ["A", "B"]); + + const el1 = { + id: "A", + version: 1, + versionNonce: 1, + index: "a0", + }; + const el2 = { + id: "B", + version: 1, + versionNonce: 1, + index: "a0", + }; + testIdentical([el1, el2], [el2, el1], ["A", "B"]); + }); +}); diff --git a/packages/excalidraw/tests/data/restore.test.ts b/packages/excalidraw/tests/data/restore.test.ts index 0a0d9b1518638..973a1ee1d9695 100644 --- a/packages/excalidraw/tests/data/restore.test.ts +++ b/packages/excalidraw/tests/data/restore.test.ts @@ -72,6 +72,7 @@ describe("restoreElements", () => { expect(restoredText).toMatchSnapshot({ seed: expect.any(Number), + versionNonce: expect.any(Number), }); }); @@ -109,7 +110,10 @@ describe("restoreElements", () => { null, )[0] as ExcalidrawFreeDrawElement; - expect(restoredFreedraw).toMatchSnapshot({ seed: expect.any(Number) }); + expect(restoredFreedraw).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); }); it("should restore line and draw elements correctly", () => { @@ -129,8 +133,14 @@ describe("restoreElements", () => { const restoredLine = restoredElements[0] as ExcalidrawLinearElement; const restoredDraw = restoredElements[1] as ExcalidrawLinearElement; - expect(restoredLine).toMatchSnapshot({ seed: expect.any(Number) }); - expect(restoredDraw).toMatchSnapshot({ seed: expect.any(Number) }); + expect(restoredLine).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(restoredDraw).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); }); it("should restore arrow element correctly", () => { @@ -140,7 +150,10 @@ describe("restoreElements", () => { const restoredArrow = restoredElements[0] as ExcalidrawLinearElement; - expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) }); + expect(restoredArrow).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); }); it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => { @@ -270,9 +283,18 @@ describe("restoreElements", () => { const restoredElements = restore.restoreElements(elements, null); - expect(restoredElements[0]).toMatchSnapshot({ seed: expect.any(Number) }); - expect(restoredElements[1]).toMatchSnapshot({ seed: expect.any(Number) }); - expect(restoredElements[2]).toMatchSnapshot({ seed: expect.any(Number) }); + expect(restoredElements[0]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(restoredElements[1]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(restoredElements[2]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); }); it("bump versions of local duplicate elements when supplied", () => { @@ -290,12 +312,11 @@ describe("restoreElements", () => { expect(restoredElements).toEqual([ expect.objectContaining({ id: rectangle.id, - version: rectangle_modified.version + 1, + version: rectangle_modified.version + 2, }), expect.objectContaining({ id: ellipse.id, - version: ellipse.version, - versionNonce: ellipse.versionNonce, + version: ellipse.version + 1, }), ]); }); @@ -549,11 +570,10 @@ describe("restore", () => { rectangle.versionNonce, ); expect(restoredData.elements).toEqual([ - expect.objectContaining({ version: rectangle_modified.version + 1 }), + expect.objectContaining({ version: rectangle_modified.version + 2 }), expect.objectContaining({ id: ellipse.id, - version: ellipse.version, - versionNonce: ellipse.versionNonce, + version: ellipse.version + 1, }), ]); }); diff --git a/packages/excalidraw/tests/fixtures/elementFixture.ts b/packages/excalidraw/tests/fixtures/elementFixture.ts index ddd7b8b9dec57..a22cfd45de836 100644 --- a/packages/excalidraw/tests/fixtures/elementFixture.ts +++ b/packages/excalidraw/tests/fixtures/elementFixture.ts @@ -17,6 +17,7 @@ const elementBase: Omit = { groupIds: [], frameId: null, roundness: null, + index: null, seed: 1041657908, version: 120, versionNonce: 1188004276, diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx index bd141f6bee1eb..b51e12f26b35c 100644 --- a/packages/excalidraw/tests/flip.test.tsx +++ b/packages/excalidraw/tests/flip.test.tsx @@ -412,7 +412,7 @@ describe("ellipse", () => { describe("arrow", () => { it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => { const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.app.scene.replaceAllElements([arrow]); + h.elements = [arrow]; h.app.setState({ selectedElementIds: { [arrow.id]: true } }); await checkHorizontalFlip( MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, @@ -421,7 +421,7 @@ describe("arrow", () => { it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => { const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.app.scene.replaceAllElements([arrow]); + h.elements = [arrow]; h.app.setState({ selectedElementIds: { [arrow.id]: true } }); await checkVerticalFlip(50); @@ -431,7 +431,7 @@ describe("arrow", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.state.selectedElementIds = { ...h.state.selectedElementIds, [line.id]: true, @@ -450,7 +450,7 @@ describe("arrow", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.state.selectedElementIds = { ...h.state.selectedElementIds, [line.id]: true, @@ -468,7 +468,7 @@ describe("arrow", () => { //TODO: elements with curve outside minMax points have a wrong bounding box!!! it.skip("flips an unrotated arrow horizontally with line outside min/max points bounds", async () => { const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); - h.app.scene.replaceAllElements([arrow]); + h.elements = [arrow]; h.app.setState({ selectedElementIds: { [arrow.id]: true } }); await checkHorizontalFlip( @@ -482,7 +482,7 @@ describe("arrow", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); mutateElement(line, { angle: originalAngle }); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkRotatedVerticalFlip( @@ -494,7 +494,7 @@ describe("arrow", () => { //TODO: elements with curve outside minMax points have a wrong bounding box!!! it.skip("flips an unrotated arrow vertically with line outside min/max points bounds", async () => { const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); - h.app.scene.replaceAllElements([arrow]); + h.elements = [arrow]; h.app.setState({ selectedElementIds: { [arrow.id]: true } }); await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); @@ -506,7 +506,7 @@ describe("arrow", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); mutateElement(line, { angle: originalAngle }); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkRotatedVerticalFlip( @@ -542,7 +542,7 @@ describe("arrow", () => { describe("line", () => { it("flips an unrotated line horizontally with line inside min/max points bounds", async () => { const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkHorizontalFlip( @@ -552,7 +552,7 @@ describe("line", () => { it("flips an unrotated line vertically with line inside min/max points bounds", async () => { const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); @@ -567,7 +567,7 @@ describe("line", () => { //TODO: elements with curve outside minMax points have a wrong bounding box it.skip("flips an unrotated line horizontally with line outside min/max points bounds", async () => { const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkHorizontalFlip( @@ -578,7 +578,7 @@ describe("line", () => { //TODO: elements with curve outside minMax points have a wrong bounding box it.skip("flips an unrotated line vertically with line outside min/max points bounds", async () => { const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); @@ -590,7 +590,7 @@ describe("line", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); mutateElement(line, { angle: originalAngle }); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkRotatedHorizontalFlip( @@ -605,7 +605,7 @@ describe("line", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); mutateElement(line, { angle: originalAngle }); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.app.setState({ selectedElementIds: { [line.id]: true } }); await checkRotatedVerticalFlip( @@ -623,7 +623,7 @@ describe("line", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.state.selectedElementIds = { ...h.state.selectedElementIds, [line.id]: true, @@ -642,7 +642,7 @@ describe("line", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.app.scene.replaceAllElements([line]); + h.elements = [line]; h.state.selectedElementIds = { ...h.state.selectedElementIds, [line.id]: true, diff --git a/packages/excalidraw/tests/fractionalIndex.test.ts b/packages/excalidraw/tests/fractionalIndex.test.ts new file mode 100644 index 0000000000000..b4d19aadcf452 --- /dev/null +++ b/packages/excalidraw/tests/fractionalIndex.test.ts @@ -0,0 +1,774 @@ +/* eslint-disable no-lone-blocks */ +import { + syncInvalidIndices, + syncMovedIndices, + validateFractionalIndices, +} from "../fractionalIndex"; +import { API } from "./helpers/api"; +import { arrayToMap } from "../utils"; +import { InvalidFractionalIndexError } from "../errors"; +import { ExcalidrawElement, FractionalIndex } from "../element/types"; +import { deepCopyElement } from "../element/newElement"; +import { generateKeyBetween } from "fractional-indexing"; + +describe("sync invalid indices with array order", () => { + describe("should NOT sync empty array", () => { + testMovedIndicesSync({ + elements: [], + movedElements: [], + expect: { + unchangedElements: [], + validInput: true, + }, + }); + + testInvalidIndicesSync({ + elements: [], + expect: { + unchangedElements: [], + validInput: true, + }, + }); + }); + + describe("should NOT sync when index is well defined", () => { + testMovedIndicesSync({ + elements: [{ id: "A", index: "a1" }], + movedElements: [], + expect: { + unchangedElements: ["A"], + validInput: true, + }, + }); + + testInvalidIndicesSync({ + elements: [{ id: "A", index: "a1" }], + expect: { + unchangedElements: ["A"], + validInput: true, + }, + }); + }); + + describe("should NOT sync when indices are well defined", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a3" }, + ], + movedElements: [], + expect: { + unchangedElements: ["A", "B", "C"], + validInput: true, + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a3" }, + ], + expect: { + unchangedElements: ["A", "B", "C"], + validInput: true, + }, + }); + }); + + describe("should NOT sync index when it is already valid", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a4" }, + ], + movedElements: ["A"], + expect: { + validInput: true, + unchangedElements: ["A", "B"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a4" }, + ], + movedElements: ["B"], + expect: { + validInput: true, + unchangedElements: ["A", "B"], + }, + }); + }); + + describe("should NOT sync indices when they are already valid", () => { + { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + movedElements: ["B", "C"], + expect: { + // this should not sync 'C' + unchangedElements: ["A", "C"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + movedElements: ["A", "B"], + expect: { + // but this should sync 'A' as it's invalid! + unchangedElements: ["C"], + }, + }); + } + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a0" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + { id: "D", index: "a1" }, + { id: "E", index: "a2" }, + ], + movedElements: ["B", "D", "E"], + expect: { + // should not sync 'E' + unchangedElements: ["A", "C", "E"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", index: "a0" }, + { id: "D", index: "a2" }, + { id: "E" }, + { id: "F", index: "a3" }, + { id: "G" }, + { id: "H", index: "a1" }, + { id: "I", index: "a2" }, + { id: "J" }, + ], + movedElements: ["A", "B", "D", "E", "F", "G", "J"], + expect: { + // should not sync 'D' and 'F' + unchangedElements: ["C", "D", "F"], + }, + }); + }); + + describe("should sync when fractional index is not defined", () => { + testMovedIndicesSync({ + elements: [{ id: "A" }], + movedElements: ["A"], + expect: { + unchangedElements: [], + }, + }); + + testInvalidIndicesSync({ + elements: [{ id: "A" }], + expect: { + unchangedElements: [], + }, + }); + }); + + describe("should sync when fractional indices are duplicated", () => { + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should sync when a fractional index is out of order", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a1" }, + ], + movedElements: ["B"], + expect: { + unchangedElements: ["A"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a1" }, + ], + movedElements: ["A"], + expect: { + unchangedElements: ["B"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should sync when fractional indices are out of order", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a3" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + movedElements: ["B", "C"], + expect: { + unchangedElements: ["A"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a3" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should sync when incorrect fractional index is in between correct ones ", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + movedElements: ["B"], + expect: { + unchangedElements: ["A", "C"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + expect: { + unchangedElements: ["A", "C"], + }, + }); + }); + + describe("should sync when incorrect fractional index is on top and duplicated below", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + movedElements: ["C"], + expect: { + unchangedElements: ["A", "B"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + expect: { + unchangedElements: ["A", "B"], + }, + }); + }); + + describe("should sync when given a mix of duplicate / invalid indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a0" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + { id: "D", index: "a1" }, + { id: "E", index: "a2" }, + ], + movedElements: ["C", "D", "E"], + expect: { + unchangedElements: ["A", "B"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a0" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + { id: "D", index: "a1" }, + { id: "E", index: "a2" }, + ], + expect: { + unchangedElements: ["A", "B"], + }, + }); + }); + + describe("should sync when given a mix of undefined / invalid indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", index: "a0" }, + { id: "D", index: "a2" }, + { id: "E" }, + { id: "F", index: "a3" }, + { id: "G" }, + { id: "H", index: "a1" }, + { id: "I", index: "a2" }, + { id: "J" }, + ], + movedElements: ["A", "B", "E", "G", "H", "I", "J"], + expect: { + unchangedElements: ["C", "D", "F"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", index: "a0" }, + { id: "D", index: "a2" }, + { id: "E" }, + { id: "F", index: "a3" }, + { id: "G" }, + { id: "H", index: "a1" }, + { id: "I", index: "a2" }, + { id: "J" }, + ], + expect: { + unchangedElements: ["C", "D", "F"], + }, + }); + }); + + describe("should generate fractions for explicitly moved elements", () => { + describe("should generate a fraction between 'A' and 'C'", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + // doing actual fractions, without jitter 'a1' becomes 'a1V' + // as V is taken as the charset's middle-right value + { id: "B", index: "a1" }, + { id: "C", index: "a2" }, + ], + movedElements: ["B"], + expect: { + unchangedElements: ["A", "C"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + { id: "C", index: "a2" }, + ], + expect: { + // as above, B will become fractional + unchangedElements: ["A", "C"], + }, + }); + }); + + describe("should generate fractions given duplicated indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + movedElements: ["B", "C", "D", "E", "F"], + expect: { + unchangedElements: ["A", "G"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + movedElements: ["A", "C", "D", "E", "G"], + expect: { + unchangedElements: ["B", "F"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + movedElements: ["B", "C", "D", "F", "G"], + expect: { + unchangedElements: ["A", "E"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + expect: { + // notice fallback considers first item (E) as a valid one + unchangedElements: ["A", "E"], + }, + }); + }); + }); + + describe("should be able to sync 20K invalid indices", () => { + const length = 20_000; + + describe("should sync all empty indices", () => { + const elements = Array.from({ length }).map((_, index) => ({ + id: `A_${index}`, + })); + + testMovedIndicesSync({ + // elements without fractional index + elements, + movedElements: Array.from({ length }).map((_, index) => `A_${index}`), + expect: { + unchangedElements: [], + }, + }); + + testInvalidIndicesSync({ + // elements without fractional index + elements, + expect: { + unchangedElements: [], + }, + }); + }); + + describe("should sync all but last index given a growing array of indices", () => { + let lastIndex: string | null = null; + + const elements = Array.from({ length }).map((_, index) => { + // going up from 'a0' + lastIndex = generateKeyBetween(lastIndex, null); + + return { + id: `A_${index}`, + // assigning the last generated index, so sync can go down from there + // without jitter lastIndex is 'c4BZ' for 20000th element + index: index === length - 1 ? lastIndex : undefined, + }; + }); + const movedElements = Array.from({ length }).map( + (_, index) => `A_${index}`, + ); + // remove last element + movedElements.pop(); + + testMovedIndicesSync({ + elements, + movedElements, + expect: { + unchangedElements: [`A_${length - 1}`], + }, + }); + + testInvalidIndicesSync({ + elements, + expect: { + unchangedElements: [`A_${length - 1}`], + }, + }); + }); + + describe("should sync all but first index given a declining array of indices", () => { + let lastIndex: string | null = null; + + const elements = Array.from({ length }).map((_, index) => { + // going down from 'a0' + lastIndex = generateKeyBetween(null, lastIndex); + + return { + id: `A_${index}`, + // without jitter lastIndex is 'XvoR' for 20000th element + index: lastIndex, + }; + }); + const movedElements = Array.from({ length }).map( + (_, index) => `A_${index}`, + ); + // remove first element + movedElements.shift(); + + testMovedIndicesSync({ + elements, + movedElements, + expect: { + unchangedElements: [`A_0`], + }, + }); + + testInvalidIndicesSync({ + elements, + expect: { + unchangedElements: [`A_0`], + }, + }); + }); + }); + + describe("should automatically fallback to fixing all invalid indices", () => { + describe("should fallback to syncing duplicated indices when moved elements are empty", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + { id: "C", index: "a1" }, + ], + // the validation will throw as nothing was synced + // therefore it will lead to triggering the fallback and fixing all invalid indices + movedElements: [], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should fallback to syncing undefined / invalid indices when moved elements are empty", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B" }, + { id: "C", index: "a0" }, + ], + // since elements are invalid, this will fail the validation + // leading to fallback fixing "B" and "C" + movedElements: [], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should fallback to syncing unordered indices when moved element is invalid", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + movedElements: ["A"], + expect: { + unchangedElements: ["A", "B"], + }, + }); + }); + + describe("should fallback when trying to generate an index in between unordered elements", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B" }, + { id: "C", index: "a1" }, + ], + // 'B' is invalid, but so is 'C', which was not marked as moved + // therefore it will try to generate a key between 'a2' and 'a1' + // which it cannot do, thus will throw during generation and automatically fallback + movedElements: ["B"], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should fallback when trying to generate an index in between duplicate indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B" }, + { id: "C" }, + { id: "D", index: "a01" }, + { id: "E", index: "a01" }, + { id: "F", index: "a01" }, + { id: "G" }, + { id: "I", index: "a03" }, + { id: "H" }, + ], + // missed "E" therefore upper bound for 'B' is a01, while lower bound is 'a02' + // therefore, similarly to above, it will fail during key generation and lead to fallback + movedElements: ["B", "C", "D", "F", "G", "H"], + expect: { + unchangedElements: ["A", "I"], + }, + }); + }); + }); +}); + +function testMovedIndicesSync(args: { + elements: { id: string; index?: string }[]; + movedElements: string[]; + expect: { + unchangedElements: string[]; + validInput?: true; + }; +}) { + const [elements, movedElements] = prepareArguments( + args.elements, + args.movedElements, + ); + const expectUnchangedElements = arrayToMap( + args.expect.unchangedElements.map((x) => ({ id: x })), + ); + + test( + "should sync invalid indices of moved elements or fallback", + elements, + movedElements, + expectUnchangedElements, + args.expect.validInput, + ); +} + +function testInvalidIndicesSync(args: { + elements: { id: string; index?: string }[]; + expect: { + unchangedElements: string[]; + validInput?: true; + }; +}) { + const [elements] = prepareArguments(args.elements); + const expectUnchangedElements = arrayToMap( + args.expect.unchangedElements.map((x) => ({ id: x })), + ); + + test( + "should sync invalid indices of all elements", + elements, + undefined, + expectUnchangedElements, + args.expect.validInput, + ); +} + +function prepareArguments( + elementsLike: { id: string; index?: string }[], + movedElementsIds?: string[], +): [ExcalidrawElement[], Map | undefined] { + const elements = elementsLike.map((x) => + API.createElement({ id: x.id, index: x.index as FractionalIndex }), + ); + const movedMap = arrayToMap(movedElementsIds || []); + const movedElements = movedElementsIds + ? arrayToMap(elements.filter((x) => movedMap.has(x.id))) + : undefined; + + return [elements, movedElements]; +} + +function test( + name: string, + elements: ExcalidrawElement[], + movedElements: Map | undefined, + expectUnchangedElements: Map, + expectValidInput?: boolean, +) { + it(name, () => { + // ensure the input is invalid (unless the flag is on) + if (!expectValidInput) { + expect(() => + validateFractionalIndices(elements.map((x) => x.index)), + ).toThrowError(InvalidFractionalIndexError); + } + + // clone due to mutation + const clonedElements = elements.map((x) => deepCopyElement(x)); + + // act + const syncedElements = movedElements + ? syncMovedIndices(clonedElements, movedElements) + : syncInvalidIndices(clonedElements); + + expect(syncedElements.length).toBe(elements.length); + expect(() => + validateFractionalIndices(syncedElements.map((x) => x.index)), + ).not.toThrowError(InvalidFractionalIndexError); + + syncedElements.forEach((synced, index) => { + const element = elements[index]; + // ensure the order hasn't changed + expect(synced.id).toBe(element.id); + + if (expectUnchangedElements.has(synced.id)) { + // ensure we didn't mutate where we didn't want to mutate + expect(synced.index).toBe(elements[index].index); + expect(synced.version).toBe(elements[index].version); + } else { + expect(synced.index).not.toBe(elements[index].index); + // ensure we mutated just once, even with fallback triggered + expect(synced.version).toBe(elements[index].version + 1); + } + }); + }); +} diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 503ebfc0132a8..28d14b8b1070b 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -103,6 +103,7 @@ export class API { id?: string; isDeleted?: boolean; frameId?: ExcalidrawElement["id"] | null; + index?: ExcalidrawElement["index"]; groupIds?: string[]; // generic element props strokeColor?: ExcalidrawGenericElement["strokeColor"]; @@ -170,6 +171,7 @@ export class API { x, y, frameId: rest.frameId ?? null, + index: rest.index ?? null, angle: rest.angle ?? 0, strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor, backgroundColor: diff --git a/packages/excalidraw/tests/library.test.tsx b/packages/excalidraw/tests/library.test.tsx index fbffd13448d5f..79fc5088c56b1 100644 --- a/packages/excalidraw/tests/library.test.tsx +++ b/packages/excalidraw/tests/library.test.tsx @@ -211,10 +211,11 @@ describe("library menu", () => { const latestLibrary = await h.app.library.getLatestLibrary(); expect(latestLibrary.length).toBeGreaterThan(0); expect(latestLibrary.length).toBe(libraryItems.length); - expect(latestLibrary[0].elements).toEqual(libraryItems[0].elements); + const { versionNonce, ...strippedElement } = libraryItems[0]?.elements[0]; // stripped due to mutations + expect(latestLibrary[0].elements).toEqual([ + expect.objectContaining(strippedElement), + ]); }); - - expect(true).toBe(true); }); }); diff --git a/packages/excalidraw/tests/regressionTests.test.tsx b/packages/excalidraw/tests/regressionTests.test.tsx index e15a12ed2fc7a..f7103a8f7b2f1 100644 --- a/packages/excalidraw/tests/regressionTests.test.tsx +++ b/packages/excalidraw/tests/regressionTests.test.tsx @@ -562,7 +562,7 @@ describe("regression tests", () => { }); it("adjusts z order when grouping", () => { - const positions = []; + const positions: number[][] = []; UI.clickTool("rectangle"); mouse.down(10, 10); diff --git a/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap index 65012ba3e0c84..797898751d83f 100644 --- a/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap +++ b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap @@ -107,7 +107,7 @@ exports[`exportToSvg > with elements that have a link 1`] = ` exports[`exportToSvg > with exportEmbedScene 1`] = ` " - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1SPW/CMFx1MDAxMN35XHUwMDE1UbpcIuFAIJSNlqpCqtqBXHUwMDAxqVVcdTAwMDdcdTAwMTNfiFx1MDAxNcdcdTAwMGW2w4dcdTAwMTD/vbaBuETMnerBkt+9d3e+e8dOXHUwMDEwhPpQQThcdELYp5hRXCLxLuxafFx1MDAwYlJRwU2o795K1DJ1zFxc62rS6zFhXHUwMDA0uVB6MkBcYp1FwKBcdTAwMDSulaF9mXdcdTAwMTBcdTAwMWPdbVwilFjpdik3XHUwMDFm06ygnPQ3aZm8zaavn07qSHvDiaO4eVx1MDAxZmz1QdK8d5To3GBcdTAwMTFCXHKWXHUwMDAzXee6XHUwMDA1Yr5mtlePKC1FXHUwMDAxz4JcdGlcdTAwMWJ5QO740iucXHUwMDE2aylqTjwnXHUwMDFhYrzKPCejjC30gZ2ngNO8llx1MDAxMLYqLK8ttvBGp4SZsleZkuucg1I3XHUwMDFhUeGU6kPrV7a/ak7cdL99V1x1MDAxMpcwt+PlNWO/XHUwMDEzc3JJfFx1MDAxM1BcdTAwMDDEJY6j0TB5ROMm4ldcdTAwMWX1UVx1MDAxYn1cdTAwMTfcrT+KxmOE4n4yalx1MDAxOFTNzOK1S5thpsBP1Tbx4k1x00hdXHUwMDExfFx1MDAxNvmPM8qLNs9cdTAwMTituJP7alxcQnEpOFx0XHUwMDFkfur+2+7fdn9hO2CMVlxuLrYzt1x1MDAxYk2Iq2qhTX5DOZsw3FLYPd1Zc+aO1TvT2jWDbfZ46px+XHUwMDAwcU5t0CJ9 + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1Sy27CMFx1MDAxMLzzXHUwMDE1kXtFwuFdbrRUXHUwMDE1UtVcdTAwMWU4ILXqwcRcdTAwMWJixdjBdnhcYvHvtVxyxFx1MDAxMPFcdTAwMDFVVVx1MDAxZizt7M7uejyHRlx1MDAxNCGzL1x1MDAwMI1cIlx1MDAwNLuEcEZcdTAwMTXZoqbDN6A0k8Km2j7WslSJr8yMKUatXHUwMDE2l5aQSW1GXHUwMDFkjPGJXHUwMDA0XHUwMDFjViCMtmVfNo6ig79thlFH3czV+mOc5kzQ9jpZXHLeJuPXT0/1RTtb0427Vbx30zuDKt4yajKLxVx1MDAxOFdYXHUwMDA2bJmZXHUwMDFhSMSSu11cdTAwMDOijZI5PEsulVvkXHUwMDAx+1x1MDAxM0YvSJIvlSxcdTAwMDVccjVxj5BFXHUwMDFhalLG+czs+UlcdTAwMDWSZKVcdTAwMDJUmzC/rFjDK56WVuXAsiOXmVx1MDAwMK1vOLIgXHQz+9qr3H7FlHp1v8NWiqxg6uRcdTAwMTUl59eNXHUwMDA1PTe+SVjtwVx0jcjV8zVcdTAwMDD107pxvzd4xMMqXHUwMDEzfFx1MDAxMLdxXHUwMDFkfZfCe1wijodDjLvtQT+M0Vx1MDAxM+tcdTAwMDbj26aEa1xiUrvNXoJTbrYrXHUwMDBiSk6koFx1MDAwNmdcIq/XWffld3pf3ExcdTAwMTlZSUGRx4/Nfy/+di/Gf9eLwDkrNJy9aG+vXHUwMDE3XCJFMTO2vy05OVx1MDAxM21cdTAwMThsn+78feqP43snu79cdTAwMDe37OHYOP5cdTAwMDBcdTAwMDLtdtMifQ==