Pārlūkot izejas kodu

feat: make `appState.selectedElementIds` more stable (#6745)

David Luzar 2 gadi atpakaļ
vecāks
revīzija
49e4289878

+ 2 - 1
src/actions/actionBoundText.tsx

@@ -31,6 +31,7 @@ import {
 } from "../element/types";
 } from "../element/types";
 import { getSelectedElements } from "../scene";
 import { getSelectedElements } from "../scene";
 import { AppState } from "../types";
 import { AppState } from "../types";
+import { Mutable } from "../utility-types";
 import { getFontString } from "../utils";
 import { getFontString } from "../utils";
 import { register } from "./register";
 import { register } from "./register";
 
 
@@ -211,7 +212,7 @@ export const actionWrapTextInContainer = register({
       appState,
       appState,
     );
     );
     let updatedElements: readonly ExcalidrawElement[] = elements.slice();
     let updatedElements: readonly ExcalidrawElement[] = elements.slice();
-    const containerIds: AppState["selectedElementIds"] = {};
+    const containerIds: Mutable<AppState["selectedElementIds"]> = {};
 
 
     for (const textElement of selectedElements) {
     for (const textElement of selectedElements) {
       if (isTextElement(textElement)) {
       if (isTextElement(textElement)) {

+ 1 - 0
src/actions/actionDuplicateSelection.tsx

@@ -274,6 +274,7 @@ const duplicateElements = (
         ),
         ),
       },
       },
       getNonDeletedElements(finalElements),
       getNonDeletedElements(finalElements),
+      appState,
     ),
     ),
   };
   };
 };
 };

+ 0 - 7
src/actions/actionFinalize.tsx

@@ -125,13 +125,6 @@ export const actionFinalize = register({
           { x, y },
           { x, y },
         );
         );
       }
       }
-
-      if (
-        !appState.activeTool.locked &&
-        appState.activeTool.type !== "freedraw"
-      ) {
-        appState.selectedElementIds[multiPointElement.id] = true;
-      }
     }
     }
 
 
     if (
     if (

+ 12 - 2
src/actions/actionGroup.tsx

@@ -218,6 +218,7 @@ export const actionUngroup = register({
     const updateAppState = selectGroupsForSelectedElements(
     const updateAppState = selectGroupsForSelectedElements(
       { ...appState, selectedGroupIds: {} },
       { ...appState, selectedGroupIds: {} },
       getNonDeletedElements(nextElements),
       getNonDeletedElements(nextElements),
+      appState,
     );
     );
 
 
     frames.forEach((frame) => {
     frames.forEach((frame) => {
@@ -232,9 +233,18 @@ export const actionUngroup = register({
     });
     });
 
 
     // remove binded text elements from selection
     // remove binded text elements from selection
-    boundTextElementIds.forEach(
-      (id) => (updateAppState.selectedElementIds[id] = false),
+    updateAppState.selectedElementIds = Object.entries(
+      updateAppState.selectedElementIds,
+    ).reduce(
+      (acc: { [key: ExcalidrawElement["id"]]: true }, [id, selected]) => {
+        if (selected && !boundTextElementIds.includes(id)) {
+          acc[id] = true;
+        }
+        return acc;
+      },
+      {},
     );
     );
+
     return {
     return {
       appState: updateAppState,
       appState: updateAppState,
       elements: nextElements,
       elements: nextElements,

+ 1 - 0
src/actions/actionSelectAll.ts

@@ -41,6 +41,7 @@ export const actionSelectAll = register({
           selectedElementIds,
           selectedElementIds,
         },
         },
         getNonDeletedElements(elements),
         getNonDeletedElements(elements),
+        appState,
       ),
       ),
       commitToHistory: true,
       commitToHistory: true,
     };
     };

+ 206 - 129
src/components/App.tsx

@@ -315,7 +315,10 @@ import {
   updateFrameMembershipOfSelectedElements,
   updateFrameMembershipOfSelectedElements,
   isElementInFrame,
   isElementInFrame,
 } from "../frame";
 } from "../frame";
-import { excludeElementsInFramesFromSelection } from "../scene/selection";
+import {
+  excludeElementsInFramesFromSelection,
+  makeNextSelectedElementIds,
+} from "../scene/selection";
 import { actionPaste } from "../actions/actionClipboard";
 import { actionPaste } from "../actions/actionClipboard";
 import {
 import {
   actionRemoveAllElementsFromFrame,
   actionRemoveAllElementsFromFrame,
@@ -1353,6 +1356,7 @@ class App extends React.Component<AppProps, AppState> {
     this.scene.destroy();
     this.scene.destroy();
     this.library.destroy();
     this.library.destroy();
     clearTimeout(touchTimeout);
     clearTimeout(touchTimeout);
+    isSomeElementSelected.clearCache();
     touchTimeout = 0;
     touchTimeout = 0;
   }
   }
 
 
@@ -1825,7 +1829,7 @@ class App extends React.Component<AppProps, AppState> {
 
 
     if (event.touches.length === 2) {
     if (event.touches.length === 2) {
       this.setState({
       this.setState({
-        selectedElementIds: {},
+        selectedElementIds: makeNextSelectedElementIds({}, this.state),
       });
       });
     }
     }
   };
   };
@@ -1835,7 +1839,10 @@ class App extends React.Component<AppProps, AppState> {
     if (event.touches.length > 0) {
     if (event.touches.length > 0) {
       this.setState({
       this.setState({
         previousSelectedElementIds: {},
         previousSelectedElementIds: {},
-        selectedElementIds: this.state.previousSelectedElementIds,
+        selectedElementIds: makeNextSelectedElementIds(
+          this.state.previousSelectedElementIds,
+          this.state,
+        ),
       });
       });
     } else {
     } else {
       gesture.pointers.clear();
       gesture.pointers.clear();
@@ -1895,7 +1902,14 @@ class App extends React.Component<AppProps, AppState> {
         const imageElement = this.createImageElement({ sceneX, sceneY });
         const imageElement = this.createImageElement({ sceneX, sceneY });
         this.insertImageElement(imageElement, file);
         this.insertImageElement(imageElement, file);
         this.initializeImageDimensions(imageElement);
         this.initializeImageDimensions(imageElement);
-        this.setState({ selectedElementIds: { [imageElement.id]: true } });
+        this.setState({
+          selectedElementIds: makeNextSelectedElementIds(
+            {
+              [imageElement.id]: true,
+            },
+            this.state,
+          ),
+        });
 
 
         return;
         return;
       }
       }
@@ -2032,6 +2046,7 @@ class App extends React.Component<AppProps, AppState> {
           selectedGroupIds: {},
           selectedGroupIds: {},
         },
         },
         this.scene.getNonDeletedElements(),
         this.scene.getNonDeletedElements(),
+        this.state,
       ),
       ),
       () => {
       () => {
         if (opts.files) {
         if (opts.files) {
@@ -2130,8 +2145,9 @@ class App extends React.Component<AppProps, AppState> {
     }
     }
 
 
     this.setState({
     this.setState({
-      selectedElementIds: Object.fromEntries(
-        textElements.map((el) => [el.id, true]),
+      selectedElementIds: makeNextSelectedElementIds(
+        Object.fromEntries(textElements.map((el) => [el.id, true])),
+        this.state,
       ),
       ),
     });
     });
 
 
@@ -2749,7 +2765,7 @@ class App extends React.Component<AppProps, AppState> {
       } else {
       } else {
         setCursorForShape(this.canvas, this.state);
         setCursorForShape(this.canvas, this.state);
         this.setState({
         this.setState({
-          selectedElementIds: {},
+          selectedElementIds: makeNextSelectedElementIds({}, this.state),
           selectedGroupIds: {},
           selectedGroupIds: {},
           editingGroupId: null,
           editingGroupId: null,
         });
         });
@@ -2794,7 +2810,7 @@ class App extends React.Component<AppProps, AppState> {
     if (nextActiveTool.type !== "selection") {
     if (nextActiveTool.type !== "selection") {
       this.setState({
       this.setState({
         activeTool: nextActiveTool,
         activeTool: nextActiveTool,
-        selectedElementIds: {},
+        selectedElementIds: makeNextSelectedElementIds({}, this.state),
         selectedGroupIds: {},
         selectedGroupIds: {},
         editingGroupId: null,
         editingGroupId: null,
       });
       });
@@ -2831,7 +2847,7 @@ class App extends React.Component<AppProps, AppState> {
     // elements by mistake while zooming
     // elements by mistake while zooming
     if (this.isTouchScreenMultiTouchGesture()) {
     if (this.isTouchScreenMultiTouchGesture()) {
       this.setState({
       this.setState({
-        selectedElementIds: {},
+        selectedElementIds: makeNextSelectedElementIds({}, this.state),
       });
       });
     }
     }
     gesture.initialScale = this.state.zoom.value;
     gesture.initialScale = this.state.zoom.value;
@@ -2876,7 +2892,10 @@ class App extends React.Component<AppProps, AppState> {
     if (this.isTouchScreenMultiTouchGesture()) {
     if (this.isTouchScreenMultiTouchGesture()) {
       this.setState({
       this.setState({
         previousSelectedElementIds: {},
         previousSelectedElementIds: {},
-        selectedElementIds: this.state.previousSelectedElementIds,
+        selectedElementIds: makeNextSelectedElementIds(
+          this.state.previousSelectedElementIds,
+          this.state,
+        ),
       });
       });
     }
     }
     gesture.initialScale = null;
     gesture.initialScale = null;
@@ -2941,10 +2960,13 @@ class App extends React.Component<AppProps, AppState> {
             ? element.containerId
             ? element.containerId
             : element.id;
             : element.id;
           this.setState((prevState) => ({
           this.setState((prevState) => ({
-            selectedElementIds: {
-              ...prevState.selectedElementIds,
-              [elementIdToSelect]: true,
-            },
+            selectedElementIds: makeNextSelectedElementIds(
+              {
+                ...prevState.selectedElementIds,
+                [elementIdToSelect]: true,
+              },
+              prevState,
+            ),
           }));
           }));
         }
         }
         if (isDeleted) {
         if (isDeleted) {
@@ -2980,7 +3002,7 @@ class App extends React.Component<AppProps, AppState> {
 
 
   private deselectElements() {
   private deselectElements() {
     this.setState({
     this.setState({
-      selectedElementIds: {},
+      selectedElementIds: makeNextSelectedElementIds({}, this.state),
       selectedGroupIds: {},
       selectedGroupIds: {},
       editingGroupId: null,
       editingGroupId: null,
     });
     });
@@ -3291,6 +3313,7 @@ class App extends React.Component<AppProps, AppState> {
               selectedGroupIds: {},
               selectedGroupIds: {},
             },
             },
             this.scene.getNonDeletedElements(),
             this.scene.getNonDeletedElements(),
+            prevState,
           ),
           ),
         );
         );
         return;
         return;
@@ -3998,12 +4021,15 @@ class App extends React.Component<AppProps, AppState> {
           editingElement: null,
           editingElement: null,
           startBoundElement: null,
           startBoundElement: null,
           suggestedBindings: [],
           suggestedBindings: [],
-          selectedElementIds: Object.keys(this.state.selectedElementIds)
-            .filter((key) => key !== element.id)
-            .reduce((obj: { [id: string]: boolean }, key) => {
-              obj[key] = this.state.selectedElementIds[key];
-              return obj;
-            }, {}),
+          selectedElementIds: makeNextSelectedElementIds(
+            Object.keys(this.state.selectedElementIds)
+              .filter((key) => key !== element.id)
+              .reduce((obj: { [id: string]: true }, key) => {
+                obj[key] = this.state.selectedElementIds[key];
+                return obj;
+              }, {}),
+            this.state,
+          ),
         },
         },
       });
       });
       return;
       return;
@@ -4472,7 +4498,7 @@ class App extends React.Component<AppProps, AppState> {
   private clearSelectionIfNotUsingSelection = (): void => {
   private clearSelectionIfNotUsingSelection = (): void => {
     if (this.state.activeTool.type !== "selection") {
     if (this.state.activeTool.type !== "selection") {
       this.setState({
       this.setState({
-        selectedElementIds: {},
+        selectedElementIds: makeNextSelectedElementIds({}, this.state),
         selectedGroupIds: {},
         selectedGroupIds: {},
         editingGroupId: null,
         editingGroupId: null,
       });
       });
@@ -4604,9 +4630,12 @@ class App extends React.Component<AppProps, AppState> {
 
 
         if (this.state.editingLinearElement) {
         if (this.state.editingLinearElement) {
           this.setState({
           this.setState({
-            selectedElementIds: {
-              [this.state.editingLinearElement.elementId]: true,
-            },
+            selectedElementIds: makeNextSelectedElementIds(
+              {
+                [this.state.editingLinearElement.elementId]: true,
+              },
+              this.state,
+            ),
           });
           });
           // If we click on something
           // If we click on something
         } else if (hitElement != null) {
         } else if (hitElement != null) {
@@ -4634,7 +4663,7 @@ class App extends React.Component<AppProps, AppState> {
               !isElementInGroup(hitElement, this.state.editingGroupId)
               !isElementInGroup(hitElement, this.state.editingGroupId)
             ) {
             ) {
               this.setState({
               this.setState({
-                selectedElementIds: {},
+                selectedElementIds: makeNextSelectedElementIds({}, this.state),
                 selectedGroupIds: {},
                 selectedGroupIds: {},
                 editingGroupId: null,
                 editingGroupId: null,
               });
               });
@@ -4650,7 +4679,7 @@ class App extends React.Component<AppProps, AppState> {
               !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
               !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
             ) {
             ) {
               this.setState((prevState) => {
               this.setState((prevState) => {
-                const nextSelectedElementIds = {
+                const nextSelectedElementIds: { [id: string]: true } = {
                   ...prevState.selectedElementIds,
                   ...prevState.selectedElementIds,
                   [hitElement.id]: true,
                   [hitElement.id]: true,
                 };
                 };
@@ -4668,13 +4697,13 @@ class App extends React.Component<AppProps, AppState> {
                     previouslySelectedElements,
                     previouslySelectedElements,
                     hitElement.id,
                     hitElement.id,
                   ).forEach((element) => {
                   ).forEach((element) => {
-                    nextSelectedElementIds[element.id] = false;
+                    delete nextSelectedElementIds[element.id];
                   });
                   });
                 } else if (hitElement.frameId) {
                 } else if (hitElement.frameId) {
                   // if hitElement is in a frame and its frame has been selected
                   // if hitElement is in a frame and its frame has been selected
                   // disable selection for the given element
                   // disable selection for the given element
                   if (nextSelectedElementIds[hitElement.frameId]) {
                   if (nextSelectedElementIds[hitElement.frameId]) {
-                    nextSelectedElementIds[hitElement.id] = false;
+                    delete nextSelectedElementIds[hitElement.id];
                   }
                   }
                 } else {
                 } else {
                   // hitElement is neither a frame nor an element in a frame
                   // hitElement is neither a frame nor an element in a frame
@@ -4704,7 +4733,7 @@ class App extends React.Component<AppProps, AppState> {
                         framesInGroups.has(element.frameId)
                         framesInGroups.has(element.frameId)
                       ) {
                       ) {
                         // deselect element and groups containing the element
                         // deselect element and groups containing the element
-                        nextSelectedElementIds[element.id] = false;
+                        delete nextSelectedElementIds[element.id];
                         element.groupIds
                         element.groupIds
                           .flatMap((gid) =>
                           .flatMap((gid) =>
                             getElementsInGroup(
                             getElementsInGroup(
@@ -4712,10 +4741,9 @@ class App extends React.Component<AppProps, AppState> {
                               gid,
                               gid,
                             ),
                             ),
                           )
                           )
-                          .forEach(
-                            (element) =>
-                              (nextSelectedElementIds[element.id] = false),
-                          );
+                          .forEach((element) => {
+                            delete nextSelectedElementIds[element.id];
+                          });
                       }
                       }
                     });
                     });
                   }
                   }
@@ -4728,6 +4756,7 @@ class App extends React.Component<AppProps, AppState> {
                     showHyperlinkPopup: hitElement.link ? "info" : false,
                     showHyperlinkPopup: hitElement.link ? "info" : false,
                   },
                   },
                   this.scene.getNonDeletedElements(),
                   this.scene.getNonDeletedElements(),
+                  prevState,
                 );
                 );
               });
               });
               pointerDownState.hit.wasAddedToSelection = true;
               pointerDownState.hit.wasAddedToSelection = true;
@@ -4844,12 +4873,18 @@ class App extends React.Component<AppProps, AppState> {
       frameId: topLayerFrame ? topLayerFrame.id : null,
       frameId: topLayerFrame ? topLayerFrame.id : null,
     });
     });
 
 
-    this.setState((prevState) => ({
-      selectedElementIds: {
+    this.setState((prevState) => {
+      const nextSelectedElementIds = {
         ...prevState.selectedElementIds,
         ...prevState.selectedElementIds,
-        [element.id]: false,
-      },
-    }));
+      };
+      delete nextSelectedElementIds[element.id];
+      return {
+        selectedElementIds: makeNextSelectedElementIds(
+          nextSelectedElementIds,
+          prevState,
+        ),
+      };
+    });
 
 
     const pressures = element.simulatePressure
     const pressures = element.simulatePressure
       ? element.pressures
       ? element.pressures
@@ -4945,10 +4980,13 @@ class App extends React.Component<AppProps, AppState> {
       }
       }
 
 
       this.setState((prevState) => ({
       this.setState((prevState) => ({
-        selectedElementIds: {
-          ...prevState.selectedElementIds,
-          [multiElement.id]: true,
-        },
+        selectedElementIds: makeNextSelectedElementIds(
+          {
+            ...prevState.selectedElementIds,
+            [multiElement.id]: true,
+          },
+          prevState,
+        ),
       }));
       }));
       // clicking outside commit zone → update reference for last committed
       // clicking outside commit zone → update reference for last committed
       // point
       // point
@@ -4999,12 +5037,18 @@ class App extends React.Component<AppProps, AppState> {
         locked: false,
         locked: false,
         frameId: topLayerFrame ? topLayerFrame.id : null,
         frameId: topLayerFrame ? topLayerFrame.id : null,
       });
       });
-      this.setState((prevState) => ({
-        selectedElementIds: {
+      this.setState((prevState) => {
+        const nextSelectedElementIds = {
           ...prevState.selectedElementIds,
           ...prevState.selectedElementIds,
-          [element.id]: false,
-        },
-      }));
+        };
+        delete nextSelectedElementIds[element.id];
+        return {
+          selectedElementIds: makeNextSelectedElementIds(
+            nextSelectedElementIds,
+            prevState,
+          ),
+        };
+      });
       mutateElement(element, {
       mutateElement(element, {
         points: [...element.points, [0, 0]],
         points: [...element.points, [0, 0]],
       });
       });
@@ -5378,15 +5422,16 @@ class App extends React.Component<AppProps, AppState> {
             const oldIdToDuplicatedId = new Map();
             const oldIdToDuplicatedId = new Map();
             const hitElement = pointerDownState.hit.element;
             const hitElement = pointerDownState.hit.element;
             const elements = this.scene.getElementsIncludingDeleted();
             const elements = this.scene.getElementsIncludingDeleted();
-            const selectedElementIds: Array<ExcalidrawElement["id"]> =
+            const selectedElementIds = new Set(
               getSelectedElements(elements, this.state, {
               getSelectedElements(elements, this.state, {
                 includeBoundTextElement: true,
                 includeBoundTextElement: true,
                 includeElementsInFrames: true,
                 includeElementsInFrames: true,
-              }).map((element) => element.id);
+              }).map((element) => element.id),
+            );
 
 
             for (const element of elements) {
             for (const element of elements) {
               if (
               if (
-                selectedElementIds.includes(element.id) ||
+                selectedElementIds.has(element.id) ||
                 // case: the state.selectedElementIds might not have been
                 // case: the state.selectedElementIds might not have been
                 // updated yet by the time this mousemove event is fired
                 // updated yet by the time this mousemove event is fired
                 (element.id === hitElement?.id &&
                 (element.id === hitElement?.id &&
@@ -5524,14 +5569,9 @@ class App extends React.Component<AppProps, AppState> {
                   },
                   },
                 },
                 },
                 this.scene.getNonDeletedElements(),
                 this.scene.getNonDeletedElements(),
+                prevState,
               ),
               ),
             );
             );
-          } else {
-            this.setState({
-              selectedElementIds: {},
-              selectedGroupIds: {},
-              editingGroupId: null,
-            });
           }
           }
         }
         }
         // box-select line editor points
         // box-select line editor points
@@ -5547,28 +5587,29 @@ class App extends React.Component<AppProps, AppState> {
             elements,
             elements,
             draggingElement,
             draggingElement,
           );
           );
-          this.setState((prevState) =>
-            selectGroupsForSelectedElements(
+          this.setState((prevState) => {
+            const nextSelectedElementIds = elementsWithinSelection.reduce(
+              (acc: Record<ExcalidrawElement["id"], true>, element) => {
+                acc[element.id] = true;
+                return acc;
+              },
+              {},
+            );
+
+            if (pointerDownState.hit.element) {
+              // if using ctrl/cmd, select the hitElement only if we
+              // haven't box-selected anything else
+              if (!elementsWithinSelection.length) {
+                nextSelectedElementIds[pointerDownState.hit.element.id] = true;
+              } else {
+                delete nextSelectedElementIds[pointerDownState.hit.element.id];
+              }
+            }
+
+            return selectGroupsForSelectedElements(
               {
               {
                 ...prevState,
                 ...prevState,
-                selectedElementIds: {
-                  ...prevState.selectedElementIds,
-                  ...elementsWithinSelection.reduce(
-                    (acc: Record<ExcalidrawElement["id"], true>, element) => {
-                      acc[element.id] = true;
-                      return acc;
-                    },
-                    {},
-                  ),
-                  ...(pointerDownState.hit.element
-                    ? {
-                        // if using ctrl/cmd, select the hitElement only if we
-                        // haven't box-selected anything else
-                        [pointerDownState.hit.element.id]:
-                          !elementsWithinSelection.length,
-                      }
-                    : null),
-                },
+                selectedElementIds: nextSelectedElementIds,
                 showHyperlinkPopup:
                 showHyperlinkPopup:
                   elementsWithinSelection.length === 1 &&
                   elementsWithinSelection.length === 1 &&
                   elementsWithinSelection[0].link
                   elementsWithinSelection[0].link
@@ -5585,8 +5626,9 @@ class App extends React.Component<AppProps, AppState> {
                     : null,
                     : null,
               },
               },
               this.scene.getNonDeletedElements(),
               this.scene.getNonDeletedElements(),
-            ),
-          );
+              prevState,
+            );
+          });
         }
         }
       }
       }
     });
     });
@@ -5780,7 +5822,12 @@ class App extends React.Component<AppProps, AppState> {
         try {
         try {
           this.initializeImageDimensions(imageElement);
           this.initializeImageDimensions(imageElement);
           this.setState(
           this.setState(
-            { selectedElementIds: { [imageElement.id]: true } },
+            {
+              selectedElementIds: makeNextSelectedElementIds(
+                { [imageElement.id]: true },
+                this.state,
+              ),
+            },
             () => {
             () => {
               this.actionManager.executeAction(actionFinalize);
               this.actionManager.executeAction(actionFinalize);
             },
             },
@@ -5844,10 +5891,13 @@ class App extends React.Component<AppProps, AppState> {
               activeTool: updateActiveTool(this.state, {
               activeTool: updateActiveTool(this.state, {
                 type: "selection",
                 type: "selection",
               }),
               }),
-              selectedElementIds: {
-                ...prevState.selectedElementIds,
-                [draggingElement.id]: true,
-              },
+              selectedElementIds: makeNextSelectedElementIds(
+                {
+                  ...prevState.selectedElementIds,
+                  [draggingElement.id]: true,
+                },
+                prevState,
+              ),
               selectedLinearElement: new LinearElementEditor(
               selectedLinearElement: new LinearElementEditor(
                 draggingElement,
                 draggingElement,
                 this.scene,
                 this.scene,
@@ -6141,31 +6191,37 @@ class App extends React.Component<AppProps, AppState> {
         if (childEvent.shiftKey && !this.state.editingLinearElement) {
         if (childEvent.shiftKey && !this.state.editingLinearElement) {
           if (this.state.selectedElementIds[hitElement.id]) {
           if (this.state.selectedElementIds[hitElement.id]) {
             if (isSelectedViaGroup(this.state, hitElement)) {
             if (isSelectedViaGroup(this.state, hitElement)) {
-              // We want to unselect all groups hitElement is part of
-              // as well as all elements that are part of the groups
-              // hitElement is part of
-              const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
-                .flatMap((groupId) =>
-                  getElementsInGroup(
-                    this.scene.getNonDeletedElements(),
-                    groupId,
-                  ),
-                )
-                .map((element) => ({ [element.id]: false }))
-                .reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
-
-              this.setState((_prevState) => ({
-                selectedGroupIds: {
-                  ..._prevState.selectedElementIds,
-                  ...hitElement.groupIds
-                    .map((gId) => ({ [gId]: false }))
-                    .reduce((prev, acc) => ({ ...prev, ...acc }), {}),
-                },
-                selectedElementIds: {
+              this.setState((_prevState) => {
+                const nextSelectedElementIds = {
                   ..._prevState.selectedElementIds,
                   ..._prevState.selectedElementIds,
-                  ...idsOfSelectedElementsThatAreInGroups,
-                },
-              }));
+                };
+
+                // We want to unselect all groups hitElement is part of
+                // as well as all elements that are part of the groups
+                // hitElement is part of
+                for (const groupedElement of hitElement.groupIds.flatMap(
+                  (groupId) =>
+                    getElementsInGroup(
+                      this.scene.getNonDeletedElements(),
+                      groupId,
+                    ),
+                )) {
+                  delete nextSelectedElementIds[groupedElement.id];
+                }
+
+                return {
+                  selectedGroupIds: {
+                    ..._prevState.selectedElementIds,
+                    ...hitElement.groupIds
+                      .map((gId) => ({ [gId]: false }))
+                      .reduce((prev, acc) => ({ ...prev, ...acc }), {}),
+                  },
+                  selectedElementIds: makeNextSelectedElementIds(
+                    nextSelectedElementIds,
+                    _prevState,
+                  ),
+                };
+              });
               // if not gragging a linear element point (outside editor)
               // if not gragging a linear element point (outside editor)
             } else if (!this.state.selectedLinearElement?.isDragging) {
             } else if (!this.state.selectedLinearElement?.isDragging) {
               // remove element from selection while
               // remove element from selection while
@@ -6174,8 +6230,8 @@ class App extends React.Component<AppProps, AppState> {
               this.setState((prevState) => {
               this.setState((prevState) => {
                 const newSelectedElementIds = {
                 const newSelectedElementIds = {
                   ...prevState.selectedElementIds,
                   ...prevState.selectedElementIds,
-                  [hitElement!.id]: false,
                 };
                 };
+                delete newSelectedElementIds[hitElement!.id];
                 const newSelectedElements = getSelectedElements(
                 const newSelectedElements = getSelectedElements(
                   this.scene.getNonDeletedElements(),
                   this.scene.getNonDeletedElements(),
                   { ...prevState, selectedElementIds: newSelectedElementIds },
                   { ...prevState, selectedElementIds: newSelectedElementIds },
@@ -6196,6 +6252,7 @@ class App extends React.Component<AppProps, AppState> {
                         : prevState.selectedLinearElement,
                         : prevState.selectedLinearElement,
                   },
                   },
                   this.scene.getNonDeletedElements(),
                   this.scene.getNonDeletedElements(),
+                  prevState,
                 );
                 );
               });
               });
             }
             }
@@ -6206,21 +6263,23 @@ class App extends React.Component<AppProps, AppState> {
             // when hitElement is part of a selected frame, deselect the frame
             // when hitElement is part of a selected frame, deselect the frame
             // to avoid frame and containing elements selected simultaneously
             // to avoid frame and containing elements selected simultaneously
             this.setState((prevState) => {
             this.setState((prevState) => {
-              const nextSelectedElementIds = {
+              const nextSelectedElementIds: {
+                [id: string]: true;
+              } = {
                 ...prevState.selectedElementIds,
                 ...prevState.selectedElementIds,
                 [hitElement.id]: true,
                 [hitElement.id]: true,
-                // deselect the frame
-                [hitElement.frameId!]: false,
               };
               };
+              // deselect the frame
+              delete nextSelectedElementIds[hitElement.frameId!];
 
 
               // deselect groups containing the frame
               // deselect groups containing the frame
               (this.scene.getElement(hitElement.frameId!)?.groupIds ?? [])
               (this.scene.getElement(hitElement.frameId!)?.groupIds ?? [])
                 .flatMap((gid) =>
                 .flatMap((gid) =>
                   getElementsInGroup(this.scene.getNonDeletedElements(), gid),
                   getElementsInGroup(this.scene.getNonDeletedElements(), gid),
                 )
                 )
-                .forEach(
-                  (element) => (nextSelectedElementIds[element.id] = false),
-                );
+                .forEach((element) => {
+                  delete nextSelectedElementIds[element.id];
+                });
 
 
               return selectGroupsForSelectedElements(
               return selectGroupsForSelectedElements(
                 {
                 {
@@ -6229,15 +6288,19 @@ class App extends React.Component<AppProps, AppState> {
                   showHyperlinkPopup: hitElement.link ? "info" : false,
                   showHyperlinkPopup: hitElement.link ? "info" : false,
                 },
                 },
                 this.scene.getNonDeletedElements(),
                 this.scene.getNonDeletedElements(),
+                prevState,
               );
               );
             });
             });
           } else {
           } else {
             // add element to selection while keeping prev elements selected
             // add element to selection while keeping prev elements selected
             this.setState((_prevState) => ({
             this.setState((_prevState) => ({
-              selectedElementIds: {
-                ..._prevState.selectedElementIds,
-                [hitElement!.id]: true,
-              },
+              selectedElementIds: makeNextSelectedElementIds(
+                {
+                  ..._prevState.selectedElementIds,
+                  [hitElement!.id]: true,
+                },
+                _prevState,
+              ),
             }));
             }));
           }
           }
         } else {
         } else {
@@ -6255,6 +6318,7 @@ class App extends React.Component<AppProps, AppState> {
                     : prevState.selectedLinearElement,
                     : prevState.selectedLinearElement,
               },
               },
               this.scene.getNonDeletedElements(),
               this.scene.getNonDeletedElements(),
+              prevState,
             ),
             ),
           }));
           }));
         }
         }
@@ -6279,7 +6343,7 @@ class App extends React.Component<AppProps, AppState> {
         } else {
         } else {
           // Deselect selected elements
           // Deselect selected elements
           this.setState({
           this.setState({
-            selectedElementIds: {},
+            selectedElementIds: makeNextSelectedElementIds({}, this.state),
             selectedGroupIds: {},
             selectedGroupIds: {},
             editingGroupId: null,
             editingGroupId: null,
           });
           });
@@ -6290,13 +6354,17 @@ class App extends React.Component<AppProps, AppState> {
       if (
       if (
         !activeTool.locked &&
         !activeTool.locked &&
         activeTool.type !== "freedraw" &&
         activeTool.type !== "freedraw" &&
-        draggingElement
+        draggingElement &&
+        draggingElement.type !== "selection"
       ) {
       ) {
         this.setState((prevState) => ({
         this.setState((prevState) => ({
-          selectedElementIds: {
-            ...prevState.selectedElementIds,
-            [draggingElement.id]: true,
-          },
+          selectedElementIds: makeNextSelectedElementIds(
+            {
+              ...prevState.selectedElementIds,
+              [draggingElement.id]: true,
+            },
+            prevState,
+          ),
         }));
         }));
       }
       }
 
 
@@ -6610,7 +6678,10 @@ class App extends React.Component<AppProps, AppState> {
         this.initializeImageDimensions(imageElement);
         this.initializeImageDimensions(imageElement);
         this.setState(
         this.setState(
           {
           {
-            selectedElementIds: { [imageElement.id]: true },
+            selectedElementIds: makeNextSelectedElementIds(
+              { [imageElement.id]: true },
+              this.state,
+            ),
           },
           },
           () => {
           () => {
             this.actionManager.executeAction(actionFinalize);
             this.actionManager.executeAction(actionFinalize);
@@ -6837,7 +6908,7 @@ class App extends React.Component<AppProps, AppState> {
 
 
   private clearSelection(hitElement: ExcalidrawElement | null): void {
   private clearSelection(hitElement: ExcalidrawElement | null): void {
     this.setState((prevState) => ({
     this.setState((prevState) => ({
-      selectedElementIds: {},
+      selectedElementIds: makeNextSelectedElementIds({}, prevState),
       selectedGroupIds: {},
       selectedGroupIds: {},
       // Continue editing the same group if the user selected a different
       // Continue editing the same group if the user selected a different
       // element from it
       // element from it
@@ -6849,7 +6920,7 @@ class App extends React.Component<AppProps, AppState> {
           : null,
           : null,
     }));
     }));
     this.setState({
     this.setState({
-      selectedElementIds: {},
+      selectedElementIds: makeNextSelectedElementIds({}, this.state),
       previousSelectedElementIds: this.state.selectedElementIds,
       previousSelectedElementIds: this.state.selectedElementIds,
     });
     });
   }
   }
@@ -6918,7 +6989,12 @@ class App extends React.Component<AppProps, AppState> {
         const imageElement = this.createImageElement({ sceneX, sceneY });
         const imageElement = this.createImageElement({ sceneX, sceneY });
         this.insertImageElement(imageElement, file);
         this.insertImageElement(imageElement, file);
         this.initializeImageDimensions(imageElement);
         this.initializeImageDimensions(imageElement);
-        this.setState({ selectedElementIds: { [imageElement.id]: true } });
+        this.setState({
+          selectedElementIds: makeNextSelectedElementIds(
+            { [imageElement.id]: true },
+            this.state,
+          ),
+        });
 
 
         return;
         return;
       }
       }
@@ -7043,6 +7119,7 @@ class App extends React.Component<AppProps, AppState> {
                   : null,
                   : null,
               },
               },
               this.scene.getNonDeletedElements(),
               this.scene.getNonDeletedElements(),
+              this.state,
             )
             )
           : this.state),
           : this.state),
         showHyperlinkPopup: false,
         showHyperlinkPopup: false,

+ 135 - 0
src/excalidraw-app/debug.ts

@@ -0,0 +1,135 @@
+declare global {
+  interface Window {
+    debug: typeof Debug;
+  }
+}
+
+const lessPrecise = (num: number, precision = 5) =>
+  parseFloat(num.toPrecision(precision));
+
+const getAvgFrameTime = (times: number[]) =>
+  lessPrecise(times.reduce((a, b) => a + b) / times.length);
+
+const getFps = (frametime: number) => lessPrecise(1000 / frametime);
+
+export class Debug {
+  public static DEBUG_LOG_TIMES = true;
+
+  private static TIMES_AGGR: Record<string, { t: number; times: number[] }> =
+    {};
+  private static TIMES_AVG: Record<
+    string,
+    { t: number; times: number[]; avg: number | null }
+  > = {};
+  private static LAST_DEBUG_LOG_CALL = 0;
+  private static DEBUG_LOG_INTERVAL_ID: null | number = null;
+
+  private static setupInterval = () => {
+    if (Debug.DEBUG_LOG_INTERVAL_ID === null) {
+      console.info("%c(starting perf recording)", "color: lime");
+      Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000);
+    }
+    Debug.LAST_DEBUG_LOG_CALL = Date.now();
+  };
+
+  private static debugLogger = () => {
+    if (
+      Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 &&
+      Debug.DEBUG_LOG_INTERVAL_ID !== null
+    ) {
+      window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID);
+      Debug.DEBUG_LOG_INTERVAL_ID = null;
+      for (const [name, { avg }] of Object.entries(Debug.TIMES_AVG)) {
+        if (avg != null) {
+          console.info(
+            `%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`,
+            "color: blue",
+          );
+        }
+      }
+      console.info("%c(stopping perf recording)", "color: red");
+      Debug.TIMES_AGGR = {};
+      Debug.TIMES_AVG = {};
+      return;
+    }
+    if (Debug.DEBUG_LOG_TIMES) {
+      for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) {
+        if (times.length) {
+          console.info(
+            name,
+            lessPrecise(times.reduce((a, b) => a + b)),
+            times.sort((a, b) => a - b).map((x) => lessPrecise(x)),
+          );
+          Debug.TIMES_AGGR[name] = { t, times: [] };
+        }
+      }
+      for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) {
+        if (times.length) {
+          const avgFrameTime = getAvgFrameTime(times);
+          console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`);
+          Debug.TIMES_AVG[name] = {
+            t,
+            times: [],
+            avg:
+              avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime,
+          };
+        }
+      }
+    }
+  };
+
+  public static logTime = (time?: number, name = "default") => {
+    Debug.setupInterval();
+    const now = performance.now();
+    const { t, times } = (Debug.TIMES_AGGR[name] = Debug.TIMES_AGGR[name] || {
+      t: 0,
+      times: [],
+    });
+    if (t) {
+      times.push(time != null ? time : now - t);
+    }
+    Debug.TIMES_AGGR[name].t = now;
+  };
+  public static logTimeAverage = (time?: number, name = "default") => {
+    Debug.setupInterval();
+    const now = performance.now();
+    const { t, times } = (Debug.TIMES_AVG[name] = Debug.TIMES_AVG[name] || {
+      t: 0,
+      times: [],
+    });
+    if (t) {
+      times.push(time != null ? time : now - t);
+    }
+    Debug.TIMES_AVG[name].t = now;
+  };
+
+  private static logWrapper =
+    (type: "logTime" | "logTimeAverage") =>
+    <T extends any[], R>(fn: (...args: T) => R, name = "default") => {
+      return (...args: T) => {
+        const t0 = performance.now();
+        const ret = fn(...args);
+        Debug.logTime(performance.now() - t0, name);
+        return ret;
+      };
+    };
+
+  public static logTimeWrap = Debug.logWrapper("logTime");
+  public static logTimeAverageWrap = Debug.logWrapper("logTimeAverage");
+
+  public static perfWrap = <T extends any[], R>(
+    fn: (...args: T) => R,
+    name = "default",
+  ) => {
+    return (...args: T) => {
+      // eslint-disable-next-line no-console
+      console.time(name);
+      const ret = fn(...args);
+      // eslint-disable-next-line no-console
+      console.timeEnd(name);
+      return ret;
+    };
+  };
+}
+
+window.debug = Debug;

+ 15 - 1
src/groups.ts

@@ -2,6 +2,7 @@ import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
 import { AppState } from "./types";
 import { AppState } from "./types";
 import { getSelectedElements } from "./scene";
 import { getSelectedElements } from "./scene";
 import { getBoundTextElement } from "./element/textElement";
 import { getBoundTextElement } from "./element/textElement";
+import { makeNextSelectedElementIds } from "./scene/selection";
 
 
 export const selectGroup = (
 export const selectGroup = (
   groupId: GroupId,
   groupId: GroupId,
@@ -67,13 +68,21 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
 export const selectGroupsForSelectedElements = (
 export const selectGroupsForSelectedElements = (
   appState: AppState,
   appState: AppState,
   elements: readonly NonDeleted<ExcalidrawElement>[],
   elements: readonly NonDeleted<ExcalidrawElement>[],
+  prevAppState: AppState,
 ): AppState => {
 ): AppState => {
   let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
   let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
 
 
   const selectedElements = getSelectedElements(elements, appState);
   const selectedElements = getSelectedElements(elements, appState);
 
 
   if (!selectedElements.length) {
   if (!selectedElements.length) {
-    return { ...nextAppState, editingGroupId: null };
+    return {
+      ...nextAppState,
+      editingGroupId: null,
+      selectedElementIds: makeNextSelectedElementIds(
+        nextAppState.selectedElementIds,
+        prevAppState,
+      ),
+    };
   }
   }
 
 
   for (const selectedElement of selectedElements) {
   for (const selectedElement of selectedElements) {
@@ -91,6 +100,11 @@ export const selectGroupsForSelectedElements = (
     }
     }
   }
   }
 
 
+  nextAppState.selectedElementIds = makeNextSelectedElementIds(
+    nextAppState.selectedElementIds,
+    prevAppState,
+  );
+
   return nextAppState;
   return nextAppState;
 };
 };
 
 

+ 35 - 0
src/scene/selection.test.ts

@@ -0,0 +1,35 @@
+import { makeNextSelectedElementIds } from "./selection";
+
+describe("makeNextSelectedElementIds", () => {
+  const _makeNextSelectedElementIds = (
+    selectedElementIds: { [id: string]: true },
+    prevSelectedElementIds: { [id: string]: true },
+    expectUpdated: boolean,
+  ) => {
+    const ret = makeNextSelectedElementIds(selectedElementIds, {
+      selectedElementIds: prevSelectedElementIds,
+    });
+    expect(ret === selectedElementIds).toBe(expectUpdated);
+  };
+  it("should return prevState selectedElementIds if no change", () => {
+    _makeNextSelectedElementIds({}, {}, false);
+    _makeNextSelectedElementIds({ 1: true }, { 1: true }, false);
+    _makeNextSelectedElementIds(
+      { 1: true, 2: true },
+      { 1: true, 2: true },
+      false,
+    );
+  });
+  it("should return new selectedElementIds if changed", () => {
+    // _makeNextSelectedElementIds({ 1: true }, { 1: false }, true);
+    _makeNextSelectedElementIds({ 1: true }, {}, true);
+    _makeNextSelectedElementIds({}, { 1: true }, true);
+    _makeNextSelectedElementIds({ 1: true }, { 2: true }, true);
+    _makeNextSelectedElementIds({ 1: true }, { 1: true, 2: true }, true);
+    _makeNextSelectedElementIds(
+      { 1: true, 2: true },
+      { 1: true, 3: true },
+      true,
+    );
+  });
+});

+ 51 - 5
src/scene/selection.ts

@@ -10,6 +10,7 @@ import {
   getContainingFrame,
   getContainingFrame,
   getFrameElements,
   getFrameElements,
 } from "../frame";
 } from "../frame";
+import { isShallowEqual } from "../utils";
 
 
 /**
 /**
  * Frames and their containing elements are not to be selected at the same time.
  * Frames and their containing elements are not to be selected at the same time.
@@ -88,11 +89,41 @@ export const getElementsWithinSelection = (
   return elementsInSelection;
   return elementsInSelection;
 };
 };
 
 
-export const isSomeElementSelected = (
-  elements: readonly NonDeletedExcalidrawElement[],
-  appState: Pick<AppState, "selectedElementIds">,
-): boolean =>
-  elements.some((element) => appState.selectedElementIds[element.id]);
+// FIXME move this into the editor instance to keep utility methods stateless
+export const isSomeElementSelected = (function () {
+  let lastElements: readonly NonDeletedExcalidrawElement[] | null = null;
+  let lastSelectedElementIds: AppState["selectedElementIds"] | null = null;
+  let isSelected: boolean | null = null;
+
+  const ret = (
+    elements: readonly NonDeletedExcalidrawElement[],
+    appState: Pick<AppState, "selectedElementIds">,
+  ): boolean => {
+    if (
+      isSelected != null &&
+      elements === lastElements &&
+      appState.selectedElementIds === lastSelectedElementIds
+    ) {
+      return isSelected;
+    }
+
+    isSelected = elements.some(
+      (element) => appState.selectedElementIds[element.id],
+    );
+    lastElements = elements;
+    lastSelectedElementIds = appState.selectedElementIds;
+
+    return isSelected;
+  };
+
+  ret.clearCache = () => {
+    lastElements = null;
+    lastSelectedElementIds = null;
+    isSelected = null;
+  };
+
+  return ret;
+})();
 
 
 /**
 /**
  * Returns common attribute (picked by `getAttribute` callback) of selected
  * Returns common attribute (picked by `getAttribute` callback) of selected
@@ -161,3 +192,18 @@ export const getTargetElements = (
     : getSelectedElements(elements, appState, {
     : getSelectedElements(elements, appState, {
         includeBoundTextElement: true,
         includeBoundTextElement: true,
       });
       });
+
+/**
+ * returns prevState's selectedElementids if no change from previous, so as to
+ * retain reference identity for memoization
+ */
+export const makeNextSelectedElementIds = (
+  nextSelectedElementIds: AppState["selectedElementIds"],
+  prevState: Pick<AppState, "selectedElementIds">,
+) => {
+  if (isShallowEqual(prevState.selectedElementIds, nextSelectedElementIds)) {
+    return prevState.selectedElementIds;
+  }
+
+  return nextSelectedElementIds;
+};

+ 0 - 13
src/tests/__snapshots__/contextmenu.test.tsx.snap

@@ -2179,7 +2179,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {
   "selectedGroupIds": Object {
@@ -2413,7 +2412,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id3": true,
           "id3": true,
@@ -4171,7 +4169,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -4399,7 +4396,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id3": true,
           "id3": true,
@@ -4479,7 +4475,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -4892,7 +4887,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -4901,8 +4895,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -5469,7 +5461,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -5478,8 +5469,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {
   "selectedGroupIds": Object {
@@ -5713,8 +5702,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id4": true,
           "id4": true,

+ 10 - 127
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -65,9 +65,6 @@ Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id3": true,
-    "id4": true,
-    "id6": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -76,7 +73,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id2": true,
     "id2": true,
-    "id7": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {
   "selectedGroupIds": Object {
@@ -443,8 +439,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id2": true,
           "id2": true,
-          "id3": true,
-          "id4": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id5": true,
           "id5": true,
@@ -618,29 +612,20 @@ Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id3": true,
-    "id5": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
-    "id0": false,
     "id1": true,
     "id1": true,
-    "id2": false,
-    "id3": true,
-    "id5": true,
-    "id6": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {
   "selectedGroupIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id3": true,
     "id4": false,
     "id4": false,
-    "id5": true,
   },
   },
   "selectedLinearElement": null,
   "selectedLinearElement": null,
   "selectionElement": null,
   "selectionElement": null,
@@ -1003,7 +988,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id2": true,
           "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id4": true,
           "id4": true,
@@ -1179,7 +1163,6 @@ Object {
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
-    "id12": true,
     "id7": true,
     "id7": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
@@ -1448,8 +1431,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id4": true,
           "id4": true,
@@ -1528,7 +1509,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id5": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -1712,8 +1692,6 @@ Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
           "id7": true,
           "id7": true,
-          "id8": true,
-          "id9": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id10": true,
           "id10": true,
@@ -1825,7 +1803,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id11": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -1934,7 +1911,6 @@ Object {
         "editingLinearElement": null,
         "editingLinearElement": null,
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
-          "id12": true,
           "id7": true,
           "id7": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
@@ -2116,7 +2092,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id1": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -2239,7 +2214,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id1": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -2347,7 +2321,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id3": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -2356,8 +2329,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id2": true,
     "id2": true,
-    "id3": true,
-    "id4": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {
   "selectedGroupIds": Object {
@@ -2724,8 +2695,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id2": true,
           "id2": true,
-          "id3": true,
-          "id4": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id5": true,
           "id5": true,
@@ -2904,7 +2873,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id1": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -3059,7 +3027,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id1": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -3394,7 +3361,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id1": true,
     "id1": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -3754,7 +3720,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id1": true,
           "id1": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -4247,7 +4212,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id1": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -4370,7 +4334,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id1": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -4478,7 +4441,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id1": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -4486,8 +4448,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id1": true,
-    "id2": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -4610,7 +4570,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id1": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -4654,8 +4613,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id1": true,
-          "id2": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -4770,7 +4727,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id2": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -5069,7 +5025,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -5522,7 +5477,6 @@ Object {
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -5875,7 +5829,6 @@ Object {
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -6423,9 +6376,7 @@ Object {
   "scrollX": 0,
   "scrollX": 0,
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
-  "selectedElementIds": Object {
-    "id1": true,
-  },
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectedLinearElement": null,
@@ -7157,7 +7108,6 @@ Object {
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -7166,8 +7116,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -7395,8 +7343,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -7536,9 +7482,7 @@ Object {
   "scrollX": 0,
   "scrollX": 0,
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
-  "selectedElementIds": Object {
-    "id7": false,
-  },
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectedLinearElement": null,
@@ -9539,9 +9483,7 @@ Object {
         "editingGroupId": null,
         "editingGroupId": null,
         "editingLinearElement": null,
         "editingLinearElement": null,
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
-        "selectedElementIds": Object {
-          "id7": false,
-        },
+        "selectedElementIds": Object {},
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
       },
       },
@@ -9948,7 +9890,6 @@ Object {
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id2": true,
     "id2": true,
-    "id3": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -9956,7 +9897,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id1": true,
     "id1": true,
-    "id4": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -10380,7 +10320,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -10389,8 +10328,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -10681,7 +10618,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -10689,7 +10625,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id1": true,
     "id1": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -10801,7 +10736,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id2": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -10938,7 +10872,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id2": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -10946,8 +10879,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id2": true,
-    "id3": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -11059,7 +10990,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id2": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -11132,8 +11062,6 @@ Object {
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
-          "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -12334,9 +12262,7 @@ Object {
   "scrollX": 0,
   "scrollX": 0,
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
-  "selectedElementIds": Object {
-    "id0": false,
-  },
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectedLinearElement": null,
@@ -12435,9 +12361,7 @@ Object {
         "editingGroupId": null,
         "editingGroupId": null,
         "editingLinearElement": null,
         "editingLinearElement": null,
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
-        "selectedElementIds": Object {
-          "id0": false,
-        },
+        "selectedElementIds": Object {},
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
       },
       },
@@ -13439,9 +13363,7 @@ Object {
   "scrollX": 0,
   "scrollX": 0,
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
-  "selectedElementIds": Object {
-    "id0": false,
-  },
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectedLinearElement": null,
@@ -13540,9 +13462,7 @@ Object {
         "editingGroupId": null,
         "editingGroupId": null,
         "editingLinearElement": null,
         "editingLinearElement": null,
         "name": "Untitled-201933152653",
         "name": "Untitled-201933152653",
-        "selectedElementIds": Object {
-          "id0": false,
-        },
+        "selectedElementIds": Object {},
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
       },
       },
@@ -13864,7 +13784,6 @@ Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id3": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -13874,8 +13793,6 @@ Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id3": true,
-    "id5": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {
   "selectedGroupIds": Object {
@@ -14347,7 +14264,6 @@ Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
           "id2": true,
           "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id4": true,
           "id4": true,
@@ -14459,8 +14375,6 @@ Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
           "id2": true,
           "id2": true,
-          "id3": true,
-          "id5": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id4": true,
           "id4": true,
@@ -14727,7 +14641,6 @@ Object {
   "pendingImageElementId": null,
   "pendingImageElementId": null,
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id3": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -14735,7 +14648,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id1": true,
     "id1": true,
-    "id4": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -15029,9 +14941,7 @@ Object {
   "scrollX": -2.916666666666668,
   "scrollX": -2.916666666666668,
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
-  "selectedElementIds": Object {
-    "id0": true,
-  },
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectedLinearElement": null,
@@ -15261,10 +15171,7 @@ Object {
   "scrollX": 0,
   "scrollX": 0,
   "scrollY": 0,
   "scrollY": 0,
   "scrolledOutside": false,
   "scrolledOutside": false,
-  "selectedElementIds": Object {
-    "id0": false,
-    "id1": true,
-  },
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectedLinearElement": null,
@@ -15451,8 +15358,6 @@ Object {
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
-    "id3": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -15461,9 +15366,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id2": true,
-    "id3": true,
-    "id4": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -15691,9 +15593,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
-          "id3": true,
-          "id4": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -15832,7 +15731,6 @@ Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id3": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -15842,7 +15740,6 @@ Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id5": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -16204,7 +16101,6 @@ Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
           "id2": true,
           "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id4": true,
           "id4": true,
@@ -16316,7 +16212,6 @@ Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
           "id2": true,
           "id2": true,
-          "id5": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -16487,7 +16382,6 @@ Object {
   "scrolledOutside": false,
   "scrolledOutside": false,
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
-    "id1": true,
   },
   },
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
@@ -16728,7 +16622,6 @@ Object {
   "selectedElementIds": Object {
   "selectedElementIds": Object {
     "id0": true,
     "id0": true,
     "id1": true,
     "id1": true,
-    "id11": true,
     "id5": true,
     "id5": true,
     "id6": true,
     "id6": true,
   },
   },
@@ -17036,8 +16929,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
-          "id2": true,
-          "id3": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id4": true,
           "id4": true,
@@ -17356,8 +17247,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id5": true,
           "id5": true,
           "id6": true,
           "id6": true,
-          "id7": true,
-          "id8": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id9": true,
           "id9": true,
@@ -18309,7 +18198,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id2": true,
           "id2": true,
-          "id4": true,
         },
         },
         "selectedGroupIds": Object {},
         "selectedGroupIds": Object {},
         "viewBackgroundColor": "#ffffff",
         "viewBackgroundColor": "#ffffff",
@@ -18418,7 +18306,6 @@ Object {
         "selectedElementIds": Object {
         "selectedElementIds": Object {
           "id0": true,
           "id0": true,
           "id2": true,
           "id2": true,
-          "id4": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id5": true,
           "id5": true,
@@ -18532,7 +18419,6 @@ Object {
           "id0": true,
           "id0": true,
           "id1": true,
           "id1": true,
           "id2": true,
           "id2": true,
-          "id7": true,
         },
         },
         "selectedGroupIds": Object {
         "selectedGroupIds": Object {
           "id3": true,
           "id3": true,
@@ -18737,7 +18623,6 @@ Object {
   "previousSelectedElementIds": Object {
   "previousSelectedElementIds": Object {
     "id1": true,
     "id1": true,
     "id2": true,
     "id2": true,
-    "id3": true,
   },
   },
   "resizingElement": null,
   "resizingElement": null,
   "scrollX": 0,
   "scrollX": 0,
@@ -19552,9 +19437,7 @@ Object {
   "scrollX": 10,
   "scrollX": 10,
   "scrollY": -10,
   "scrollY": -10,
   "scrolledOutside": false,
   "scrolledOutside": false,
-  "selectedElementIds": Object {
-    "id0": true,
-  },
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
   "selectedElementsAreBeingDragged": false,
   "selectedGroupIds": Object {},
   "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectedLinearElement": null,

+ 32 - 8
src/tests/flip.test.tsx

@@ -430,7 +430,10 @@ describe("arrow", () => {
     const expectedAngle = (7 * Math.PI) / 4;
     const expectedAngle = (7 * Math.PI) / 4;
     const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
     const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
     h.app.scene.replaceAllElements([line]);
     h.app.scene.replaceAllElements([line]);
-    h.app.state.selectedElementIds[line.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [line.id]: true,
+    };
     mutateElement(line, {
     mutateElement(line, {
       angle: originalAngle,
       angle: originalAngle,
     });
     });
@@ -446,7 +449,10 @@ describe("arrow", () => {
     const expectedAngle = (7 * Math.PI) / 4;
     const expectedAngle = (7 * Math.PI) / 4;
     const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
     const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
     h.app.scene.replaceAllElements([line]);
     h.app.scene.replaceAllElements([line]);
-    h.app.state.selectedElementIds[line.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [line.id]: true,
+    };
     mutateElement(line, {
     mutateElement(line, {
       angle: originalAngle,
       angle: originalAngle,
     });
     });
@@ -616,7 +622,10 @@ describe("line", () => {
     const expectedAngle = (7 * Math.PI) / 4;
     const expectedAngle = (7 * Math.PI) / 4;
     const line = createLinearElementWithCurveInsideMinMaxPoints("line");
     const line = createLinearElementWithCurveInsideMinMaxPoints("line");
     h.app.scene.replaceAllElements([line]);
     h.app.scene.replaceAllElements([line]);
-    h.app.state.selectedElementIds[line.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [line.id]: true,
+    };
     mutateElement(line, {
     mutateElement(line, {
       angle: originalAngle,
       angle: originalAngle,
     });
     });
@@ -632,7 +641,10 @@ describe("line", () => {
     const expectedAngle = (7 * Math.PI) / 4;
     const expectedAngle = (7 * Math.PI) / 4;
     const line = createLinearElementWithCurveInsideMinMaxPoints("line");
     const line = createLinearElementWithCurveInsideMinMaxPoints("line");
     h.app.scene.replaceAllElements([line]);
     h.app.scene.replaceAllElements([line]);
-    h.app.state.selectedElementIds[line.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [line.id]: true,
+    };
     mutateElement(line, {
     mutateElement(line, {
       angle: originalAngle,
       angle: originalAngle,
     });
     });
@@ -659,14 +671,20 @@ describe("freedraw", () => {
   it("flips an unrotated drawing horizontally correctly", async () => {
   it("flips an unrotated drawing horizontally correctly", async () => {
     const draw = createAndReturnOneDraw();
     const draw = createAndReturnOneDraw();
     // select draw, since not done automatically
     // select draw, since not done automatically
-    h.state.selectedElementIds[draw.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [draw.id]: true,
+    };
     await checkHorizontalFlip();
     await checkHorizontalFlip();
   });
   });
 
 
   it("flips an unrotated drawing vertically correctly", async () => {
   it("flips an unrotated drawing vertically correctly", async () => {
     const draw = createAndReturnOneDraw();
     const draw = createAndReturnOneDraw();
     // select draw, since not done automatically
     // select draw, since not done automatically
-    h.state.selectedElementIds[draw.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [draw.id]: true,
+    };
     await checkVerticalFlip();
     await checkVerticalFlip();
   });
   });
 
 
@@ -676,7 +694,10 @@ describe("freedraw", () => {
 
 
     const draw = createAndReturnOneDraw(originalAngle);
     const draw = createAndReturnOneDraw(originalAngle);
     // select draw, since not done automatically
     // select draw, since not done automatically
-    h.state.selectedElementIds[draw.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [draw.id]: true,
+    };
 
 
     await checkRotatedHorizontalFlip(expectedAngle);
     await checkRotatedHorizontalFlip(expectedAngle);
   });
   });
@@ -687,7 +708,10 @@ describe("freedraw", () => {
 
 
     const draw = createAndReturnOneDraw(originalAngle);
     const draw = createAndReturnOneDraw(originalAngle);
     // select draw, since not done automatically
     // select draw, since not done automatically
-    h.state.selectedElementIds[draw.id] = true;
+    h.state.selectedElementIds = {
+      ...h.state.selectedElementIds,
+      [draw.id]: true,
+    };
 
 
     await checkRotatedVerticalFlip(expectedAngle);
     await checkRotatedVerticalFlip(expectedAngle);
   });
   });

+ 1 - 0
src/tests/zindex.test.tsx

@@ -89,6 +89,7 @@ const populateElements = (
     ...selectGroupsForSelectedElements(
     ...selectGroupsForSelectedElements(
       { ...h.state, ...appState, selectedElementIds },
       { ...h.state, ...appState, selectedElementIds },
       h.elements,
       h.elements,
+      h.state,
     ),
     ),
     ...appState,
     ...appState,
     selectedElementIds,
     selectedElementIds,

+ 2 - 2
src/types.ts

@@ -181,8 +181,8 @@ export type AppState = {
   defaultSidebarDockedPreference: boolean;
   defaultSidebarDockedPreference: boolean;
 
 
   lastPointerDownWith: PointerType;
   lastPointerDownWith: PointerType;
-  selectedElementIds: { [id: string]: boolean };
-  previousSelectedElementIds: { [id: string]: boolean };
+  selectedElementIds: Readonly<{ [id: string]: true }>;
+  previousSelectedElementIds: { [id: string]: true };
   selectedElementsAreBeingDragged: boolean;
   selectedElementsAreBeingDragged: boolean;
   shouldCacheIgnoreZoom: boolean;
   shouldCacheIgnoreZoom: boolean;
   toast: { message: string; closable?: boolean; duration?: number } | null;
   toast: { message: string; closable?: boolean; duration?: number } | null;