2
0
Эх сурвалжийг харах

perf: improve new element drawing (#8340)

Co-authored-by: dwelle <[email protected]>
Ryan Di 1 жил өмнө
parent
commit
5e1ff7cafe
34 өөрчлөгдсөн 743 нэмэгдсэн , 487 устгасан
  1. 20 14
      excalidraw-app/collab/Portal.tsx
  2. 1 2
      packages/excalidraw/actions/actionFinalize.tsx
  3. 1 1
      packages/excalidraw/actions/actionHistory.tsx
  4. 12 11
      packages/excalidraw/actions/actionProperties.tsx
  5. 2 2
      packages/excalidraw/appState.ts
  6. 4 2
      packages/excalidraw/components/Actions.tsx
  7. 166 108
      packages/excalidraw/components/App.tsx
  8. 2 2
      packages/excalidraw/components/HintViewer.tsx
  9. 1 1
      packages/excalidraw/components/canvases/InteractiveCanvas.tsx
  10. 56 0
      packages/excalidraw/components/canvases/NewElementCanvas.tsx
  11. 1 1
      packages/excalidraw/data/reconcile.ts
  12. 43 23
      packages/excalidraw/element/dragElements.ts
  13. 2 1
      packages/excalidraw/element/newElement.ts
  14. 1 1
      packages/excalidraw/element/showSelectedShapeActions.ts
  15. 9 9
      packages/excalidraw/element/textWysiwyg.test.tsx
  16. 6 3
      packages/excalidraw/renderer/interactiveScene.ts
  17. 66 0
      packages/excalidraw/renderer/renderNewElementScene.ts
  18. 20 8
      packages/excalidraw/scene/Renderer.ts
  19. 8 3
      packages/excalidraw/scene/selection.ts
  20. 11 0
      packages/excalidraw/scene/types.ts
  21. 31 31
      packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
  22. 9 9
      packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap
  23. 94 94
      packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
  24. 1 1
      packages/excalidraw/tests/__snapshots__/move.test.tsx.snap
  25. 2 2
      packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap
  26. 95 95
      packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
  27. 40 20
      packages/excalidraw/tests/dragCreate.test.tsx
  28. 10 10
      packages/excalidraw/tests/elementLocking.test.tsx
  29. 5 5
      packages/excalidraw/tests/history.test.tsx
  30. 6 6
      packages/excalidraw/tests/move.test.tsx
  31. 4 4
      packages/excalidraw/tests/multiPointCreate.test.tsx
  32. 10 10
      packages/excalidraw/tests/selection.test.tsx
  33. 3 7
      packages/excalidraw/types.ts
  34. 1 1
      packages/utils/__snapshots__/export.test.ts.snap

+ 20 - 14
excalidraw-app/collab/Portal.tsx

@@ -116,20 +116,26 @@ class Portal {
       }
       }
     }
     }
 
 
-    this.collab.excalidrawAPI.updateScene({
-      elements: this.collab.excalidrawAPI
-        .getSceneElementsIncludingDeleted()
-        .map((element) => {
-          if (this.collab.fileManager.shouldUpdateImageElementStatus(element)) {
-            // this will signal collaborators to pull image data from server
-            // (using mutation instead of newElementWith otherwise it'd break
-            // in-progress dragging)
-            return newElementWith(element, { status: "saved" });
-          }
-          return element;
-        }),
-      storeAction: StoreAction.UPDATE,
-    });
+    let isChanged = false;
+    const newElements = this.collab.excalidrawAPI
+      .getSceneElementsIncludingDeleted()
+      .map((element) => {
+        if (this.collab.fileManager.shouldUpdateImageElementStatus(element)) {
+          isChanged = true;
+          // this will signal collaborators to pull image data from server
+          // (using mutation instead of newElementWith otherwise it'd break
+          // in-progress dragging)
+          return newElementWith(element, { status: "saved" });
+        }
+        return element;
+      });
+
+    if (isChanged) {
+      this.collab.excalidrawAPI.updateScene({
+        elements: newElements,
+        storeAction: StoreAction.UPDATE,
+      });
+    }
   }, FILE_UPLOAD_TIMEOUT);
   }, FILE_UPLOAD_TIMEOUT);
 
 
   broadcastScene = async (
   broadcastScene = async (

+ 1 - 2
packages/excalidraw/actions/actionFinalize.tsx

@@ -50,7 +50,6 @@ export const actionFinalize = register({
             ...appState,
             ...appState,
             cursorButton: "up",
             cursorButton: "up",
             editingLinearElement: null,
             editingLinearElement: null,
-            selectedLinearElement: null,
           },
           },
           storeAction: StoreAction.CAPTURE,
           storeAction: StoreAction.CAPTURE,
         };
         };
@@ -179,7 +178,7 @@ export const actionFinalize = register({
         newElement: null,
         newElement: null,
         selectionElement: null,
         selectionElement: null,
         multiElement: null,
         multiElement: null,
-        editingElement: null,
+        editingTextElement: null,
         startBoundElement: null,
         startBoundElement: null,
         suggestedBindings: [],
         suggestedBindings: [],
         selectedElementIds:
         selectedElementIds:

+ 1 - 1
packages/excalidraw/actions/actionHistory.tsx

@@ -21,7 +21,7 @@ const executeHistoryAction = (
   if (
   if (
     !appState.multiElement &&
     !appState.multiElement &&
     !appState.resizingElement &&
     !appState.resizingElement &&
-    !appState.editingElement &&
+    !appState.editingTextElement &&
     !appState.newElement &&
     !appState.newElement &&
     !appState.selectedElementsAreBeingDragged &&
     !appState.selectedElementsAreBeingDragged &&
     !appState.selectionElement &&
     !appState.selectionElement &&

+ 12 - 11
packages/excalidraw/actions/actionProperties.tsx

@@ -133,7 +133,7 @@ export const changeProperty = (
   return elements.map((element) => {
   return elements.map((element) => {
     if (
     if (
       selectedElementIds.get(element.id) ||
       selectedElementIds.get(element.id) ||
-      element.id === appState.editingElement?.id
+      element.id === appState.editingTextElement?.id
     ) {
     ) {
       return callback(element);
       return callback(element);
     }
     }
@@ -148,13 +148,13 @@ export const getFormValue = function <T extends Primitive>(
   isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
   isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
   defaultValue: T | ((isSomeElementSelected: boolean) => T),
   defaultValue: T | ((isSomeElementSelected: boolean) => T),
 ): T {
 ): T {
-  const editingElement = appState.editingElement;
+  const editingTextElement = appState.editingTextElement;
   const nonDeletedElements = getNonDeletedElements(elements);
   const nonDeletedElements = getNonDeletedElements(elements);
 
 
   let ret: T | null = null;
   let ret: T | null = null;
 
 
-  if (editingElement) {
-    ret = getAttribute(editingElement);
+  if (editingTextElement) {
+    ret = getAttribute(editingTextElement);
   }
   }
 
 
   if (!ret) {
   if (!ret) {
@@ -1076,19 +1076,20 @@ export const actionChangeFontFamily = register({
               // open, populate the cache from scratch
               // open, populate the cache from scratch
               cachedElementsRef.current.clear();
               cachedElementsRef.current.clear();
 
 
-              const { editingElement } = appState;
+              const { editingTextElement } = appState;
 
 
-              if (editingElement?.type === "text") {
-                // retrieve the latest version from the scene, as `editingElement` isn't mutated
-                const latestEditingElement = app.scene.getElement(
-                  editingElement.id,
+              // still check type to be safe
+              if (editingTextElement?.type === "text") {
+                // retrieve the latest version from the scene, as `editingTextElement` isn't mutated
+                const latesteditingTextElement = app.scene.getElement(
+                  editingTextElement.id,
                 );
                 );
 
 
                 // inside the wysiwyg editor
                 // inside the wysiwyg editor
                 cachedElementsRef.current.set(
                 cachedElementsRef.current.set(
-                  editingElement.id,
+                  editingTextElement.id,
                   newElementWith(
                   newElementWith(
-                    latestEditingElement || editingElement,
+                    latesteditingTextElement || editingTextElement,
                     {},
                     {},
                     true,
                     true,
                   ),
                   ),

+ 2 - 2
packages/excalidraw/appState.ts

@@ -44,7 +44,7 @@ export const getDefaultAppState = (): Omit<
     cursorButton: "up",
     cursorButton: "up",
     activeEmbeddable: null,
     activeEmbeddable: null,
     newElement: null,
     newElement: null,
-    editingElement: null,
+    editingTextElement: null,
     editingGroupId: null,
     editingGroupId: null,
     editingLinearElement: null,
     editingLinearElement: null,
     activeTool: {
     activeTool: {
@@ -165,7 +165,7 @@ const APP_STATE_STORAGE_CONF = (<
   cursorButton: { browser: true, export: false, server: false },
   cursorButton: { browser: true, export: false, server: false },
   activeEmbeddable: { browser: false, export: false, server: false },
   activeEmbeddable: { browser: false, export: false, server: false },
   newElement: { browser: false, export: false, server: false },
   newElement: { browser: false, export: false, server: false },
-  editingElement: { browser: false, export: false, server: false },
+  editingTextElement: { browser: false, export: false, server: false },
   editingGroupId: { browser: true, export: false, server: false },
   editingGroupId: { browser: true, export: false, server: false },
   editingLinearElement: { browser: false, export: false, server: false },
   editingLinearElement: { browser: false, export: false, server: false },
   activeTool: { browser: true, export: false, server: false },
   activeTool: { browser: true, export: false, server: false },

+ 4 - 2
packages/excalidraw/components/Actions.tsx

@@ -103,7 +103,9 @@ export const SelectedShapeActions = ({
   ) {
   ) {
     isSingleElementBoundContainer = true;
     isSingleElementBoundContainer = true;
   }
   }
-  const isEditing = Boolean(appState.editingElement);
+  const isEditingTextOrNewElement = Boolean(
+    appState.editingTextElement || appState.newElement,
+  );
   const device = useDevice();
   const device = useDevice();
   const isRTL = document.documentElement.getAttribute("dir") === "rtl";
   const isRTL = document.documentElement.getAttribute("dir") === "rtl";
 
 
@@ -233,7 +235,7 @@ export const SelectedShapeActions = ({
           </div>
           </div>
         </fieldset>
         </fieldset>
       )}
       )}
-      {!isEditing && targetElements.length > 0 && (
+      {!isEditingTextOrNewElement && targetElements.length > 0 && (
         <fieldset>
         <fieldset>
           <legend>{t("labels.actions")}</legend>
           <legend>{t("labels.actions")}</legend>
           <div className="buttonList">
           <div className="buttonList">

+ 166 - 108
packages/excalidraw/components/App.tsx

@@ -432,6 +432,7 @@ import { getShortcutFromShortcutName } from "../actions/shortcuts";
 import { actionTextAutoResize } from "../actions/actionTextAutoResize";
 import { actionTextAutoResize } from "../actions/actionTextAutoResize";
 import { getVisibleSceneBounds } from "../element/bounds";
 import { getVisibleSceneBounds } from "../element/bounds";
 import { isMaybeMermaidDefinition } from "../mermaid";
 import { isMaybeMermaidDefinition } from "../mermaid";
+import NewElementCanvas from "./canvases/NewElementCanvas";
 import { mutateElbowArrow } from "../element/routing";
 import { mutateElbowArrow } from "../element/routing";
 import {
 import {
   FlowChartCreator,
   FlowChartCreator,
@@ -1472,25 +1473,21 @@ class App extends React.Component<AppProps, AppState> {
         scrollY: this.state.scrollY,
         scrollY: this.state.scrollY,
         height: this.state.height,
         height: this.state.height,
         width: this.state.width,
         width: this.state.width,
-        editingElement: this.state.editingElement,
+        editingTextElement: this.state.editingTextElement,
+        newElementId: this.state.newElement?.id,
         pendingImageElementId: this.state.pendingImageElementId,
         pendingImageElementId: this.state.pendingImageElementId,
       });
       });
 
 
     const allElementsMap = this.scene.getNonDeletedElementsMap();
     const allElementsMap = this.scene.getNonDeletedElementsMap();
 
 
     const shouldBlockPointerEvents =
     const shouldBlockPointerEvents =
-      !(
-        this.state.editingElement && isLinearElement(this.state.editingElement)
-      ) &&
-      (this.state.selectionElement ||
-        this.state.newElement ||
-        this.state.selectedElementsAreBeingDragged ||
-        this.state.resizingElement ||
-        (this.state.activeTool.type === "laser" &&
-          // technically we can just test on this once we make it more safe
-          this.state.cursorButton === "down") ||
-        (this.state.editingElement &&
-          !isTextElement(this.state.editingElement)));
+      this.state.selectionElement ||
+      this.state.newElement ||
+      this.state.selectedElementsAreBeingDragged ||
+      this.state.resizingElement ||
+      (this.state.activeTool.type === "laser" &&
+        // technically we can just test on this once we make it more safe
+        this.state.cursorButton === "down");
 
 
     const firstSelectedElement = selectedElements[0];
     const firstSelectedElement = selectedElements[0];
 
 
@@ -1697,6 +1694,27 @@ class App extends React.Component<AppProps, AppState> {
                               this.flowChartCreator.pendingNodes,
                               this.flowChartCreator.pendingNodes,
                           }}
                           }}
                         />
                         />
+                        {this.state.newElement && (
+                          <NewElementCanvas
+                            appState={this.state}
+                            scale={window.devicePixelRatio}
+                            rc={this.rc}
+                            elementsMap={elementsMap}
+                            allElementsMap={allElementsMap}
+                            renderConfig={{
+                              imageCache: this.imageCache,
+                              isExporting: false,
+                              renderGrid: false,
+                              canvasBackgroundColor:
+                                this.state.viewBackgroundColor,
+                              embedsValidationStatus:
+                                this.embedsValidationStatus,
+                              elementsPendingErasure:
+                                this.elementsPendingErasure,
+                              pendingFlowchartNodes: null,
+                            }}
+                          />
+                        )}
                         <InteractiveCanvas
                         <InteractiveCanvas
                           containerRef={this.excalidrawContainerRef}
                           containerRef={this.excalidrawContainerRef}
                           canvas={this.interactiveCanvas}
                           canvas={this.interactiveCanvas}
@@ -2073,7 +2091,7 @@ class App extends React.Component<AppProps, AppState> {
 
 
     let didUpdate = false;
     let didUpdate = false;
 
 
-    let editingElement: AppState["editingElement"] | null = null;
+    let editingTextElement: AppState["editingTextElement"] | null = null;
     if (actionResult.elements) {
     if (actionResult.elements) {
       this.scene.replaceAllElements(actionResult.elements);
       this.scene.replaceAllElements(actionResult.elements);
       didUpdate = true;
       didUpdate = true;
@@ -2086,7 +2104,7 @@ class App extends React.Component<AppProps, AppState> {
       this.addNewImagesToImageCache();
       this.addNewImagesToImageCache();
     }
     }
 
 
-    if (actionResult.appState || editingElement || this.state.contextMenu) {
+    if (actionResult.appState || editingTextElement || this.state.contextMenu) {
       let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
       let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
       let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
       let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
       const theme =
       const theme =
@@ -2102,23 +2120,24 @@ class App extends React.Component<AppProps, AppState> {
         zenModeEnabled = this.props.zenModeEnabled;
         zenModeEnabled = this.props.zenModeEnabled;
       }
       }
 
 
-      editingElement = actionResult.appState?.editingElement || null;
+      editingTextElement = actionResult.appState?.editingTextElement || null;
 
 
-      // make sure editingElement points to latest element reference
-      if (actionResult.elements && editingElement) {
+      // make sure editingTextElement points to latest element reference
+      if (actionResult.elements && editingTextElement) {
         actionResult.elements.forEach((element) => {
         actionResult.elements.forEach((element) => {
           if (
           if (
-            editingElement?.id === element.id &&
-            editingElement !== element &&
-            isNonDeletedElement(element)
+            editingTextElement?.id === element.id &&
+            editingTextElement !== element &&
+            isNonDeletedElement(element) &&
+            isTextElement(element)
           ) {
           ) {
-            editingElement = element;
+            editingTextElement = element;
           }
           }
         });
         });
       }
       }
 
 
-      if (editingElement?.isDeleted) {
-        editingElement = null;
+      if (editingTextElement?.isDeleted) {
+        editingTextElement = null;
       }
       }
 
 
       this.setState((state) => {
       this.setState((state) => {
@@ -2130,7 +2149,7 @@ class App extends React.Component<AppProps, AppState> {
           // or programmatically from the host, so it will need to be
           // or programmatically from the host, so it will need to be
           // rewritten later
           // rewritten later
           contextMenu: null,
           contextMenu: null,
-          editingElement,
+          editingTextElement,
           viewModeEnabled,
           viewModeEnabled,
           zenModeEnabled,
           zenModeEnabled,
           theme,
           theme,
@@ -2709,9 +2728,9 @@ class App extends React.Component<AppProps, AppState> {
     }
     }
 
 
     // failsafe in case the state is being updated in incorrect order resulting
     // failsafe in case the state is being updated in incorrect order resulting
-    // in the editingElement being now a deleted element
-    if (this.state.editingElement?.isDeleted) {
-      this.setState({ editingElement: null });
+    // in the editingTextElement being now a deleted element
+    if (this.state.editingTextElement?.isDeleted) {
+      this.setState({ editingTextElement: null });
     }
     }
 
 
     if (
     if (
@@ -2767,7 +2786,7 @@ class App extends React.Component<AppProps, AppState> {
     }
     }
     const scrolledOutside =
     const scrolledOutside =
       // hide when editing text
       // hide when editing text
-      isTextElement(this.state.editingElement)
+      this.state.editingTextElement
         ? false
         ? false
         : !atLeastOneVisibleElement && elementsMap.size > 0;
         : !atLeastOneVisibleElement && elementsMap.size > 0;
     if (this.state.scrolledOutside !== scrolledOutside) {
     if (this.state.scrolledOutside !== scrolledOutside) {
@@ -4636,7 +4655,7 @@ class App extends React.Component<AppProps, AppState> {
 
 
         this.setState({
         this.setState({
           newElement: null,
           newElement: null,
-          editingElement: null,
+          editingTextElement: null,
         });
         });
         if (this.state.activeTool.locked) {
         if (this.state.activeTool.locked) {
           setCursorForShape(this.interactiveCanvas, this.state);
           setCursorForShape(this.interactiveCanvas, this.state);
@@ -5010,7 +5029,7 @@ class App extends React.Component<AppProps, AppState> {
         }),
         }),
       });
       });
     }
     }
-    this.setState({ editingElement: element });
+    this.setState({ editingTextElement: element });
 
 
     if (!existingTextElement) {
     if (!existingTextElement) {
       if (container && shouldBindToContainer) {
       if (container && shouldBindToContainer) {
@@ -5467,9 +5486,13 @@ class App extends React.Component<AppProps, AppState> {
             lastPoint[1],
             lastPoint[1],
           ) >= LINE_CONFIRM_THRESHOLD
           ) >= LINE_CONFIRM_THRESHOLD
         ) {
         ) {
-          mutateElement(multiElement, {
-            points: [...points, [scenePointerX - rx, scenePointerY - ry]],
-          });
+          mutateElement(
+            multiElement,
+            {
+              points: [...points, [scenePointerX - rx, scenePointerY - ry]],
+            },
+            false,
+          );
         } else {
         } else {
           setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
           setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
           // in this branch, we're inside the commit zone, and no uncommitted
           // in this branch, we're inside the commit zone, and no uncommitted
@@ -5486,9 +5509,13 @@ class App extends React.Component<AppProps, AppState> {
         ) < LINE_CONFIRM_THRESHOLD
         ) < LINE_CONFIRM_THRESHOLD
       ) {
       ) {
         setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
         setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
-        mutateElement(multiElement, {
-          points: points.slice(0, -1),
-        });
+        mutateElement(
+          multiElement,
+          {
+            points: points.slice(0, -1),
+          },
+          false,
+        );
       } else {
       } else {
         const [gridX, gridY] = getGridPoint(
         const [gridX, gridY] = getGridPoint(
           scenePointerX,
           scenePointerX,
@@ -5534,20 +5561,30 @@ class App extends React.Component<AppProps, AppState> {
             undefined,
             undefined,
             {
             {
               isDragging: true,
               isDragging: true,
+              informMutation: false,
             },
             },
           );
           );
         } else {
         } else {
           // update last uncommitted point
           // update last uncommitted point
-          mutateElement(multiElement, {
-            points: [
-              ...points.slice(0, -1),
-              [
-                lastCommittedX + dxFromLastCommitted,
-                lastCommittedY + dyFromLastCommitted,
+          mutateElement(
+            multiElement,
+            {
+              points: [
+                ...points.slice(0, -1),
+                [
+                  lastCommittedX + dxFromLastCommitted,
+                  lastCommittedY + dyFromLastCommitted,
+                ],
               ],
               ],
-            ],
-          });
+            },
+            false,
+          );
         }
         }
+
+        // in this path, we're mutating multiElement to reflect
+        // how it will be after adding pointer position as the next point
+        // trigger update here so that new element canvas renders again to reflect this
+        this.triggerRender(false);
       }
       }
 
 
       return;
       return;
@@ -5950,7 +5987,7 @@ class App extends React.Component<AppProps, AppState> {
           : {}),
           : {}),
         appState: {
         appState: {
           newElement: null,
           newElement: null,
-          editingElement: null,
+          editingTextElement: null,
           startBoundElement: null,
           startBoundElement: null,
           suggestedBindings: [],
           suggestedBindings: [],
           selectedElementIds: makeNextSelectedElementIds(
           selectedElementIds: makeNextSelectedElementIds(
@@ -6133,7 +6170,6 @@ class App extends React.Component<AppProps, AppState> {
 
 
       this.setState({
       this.setState({
         newElement: pendingImageElement as ExcalidrawNonSelectionElement,
         newElement: pendingImageElement as ExcalidrawNonSelectionElement,
-        editingElement: pendingImageElement,
         pendingImageElementId: null,
         pendingImageElementId: null,
         multiElement: null,
         multiElement: null,
       });
       });
@@ -6339,7 +6375,7 @@ class App extends React.Component<AppProps, AppState> {
           isHandToolActive(this.state) ||
           isHandToolActive(this.state) ||
           this.state.viewModeEnabled)
           this.state.viewModeEnabled)
       ) ||
       ) ||
-      isTextElement(this.state.editingElement)
+      this.state.editingTextElement
     ) {
     ) {
       return false;
       return false;
     }
     }
@@ -6883,7 +6919,7 @@ class App extends React.Component<AppProps, AppState> {
     // if we're currently still editing text, clicking outside
     // if we're currently still editing text, clicking outside
     // should only finalize it, not create another (irrespective
     // should only finalize it, not create another (irrespective
     // of state.activeTool.locked)
     // of state.activeTool.locked)
-    if (isTextElement(this.state.editingElement)) {
+    if (this.state.editingTextElement) {
       return;
       return;
     }
     }
     let sceneX = pointerDownState.origin.x;
     let sceneX = pointerDownState.origin.x;
@@ -6934,6 +6970,8 @@ class App extends React.Component<AppProps, AppState> {
       y: gridY,
       y: gridY,
     });
     });
 
 
+    const simulatePressure = event.pressure === 0.5;
+
     const element = newFreeDrawElement({
     const element = newFreeDrawElement({
       type: elementType,
       type: elementType,
       x: gridX,
       x: gridX,
@@ -6946,11 +6984,15 @@ class App extends React.Component<AppProps, AppState> {
       roughness: this.state.currentItemRoughness,
       roughness: this.state.currentItemRoughness,
       opacity: this.state.currentItemOpacity,
       opacity: this.state.currentItemOpacity,
       roundness: null,
       roundness: null,
-      simulatePressure: event.pressure === 0.5,
+      simulatePressure,
       locked: false,
       locked: false,
       frameId: topLayerFrame ? topLayerFrame.id : null,
       frameId: topLayerFrame ? topLayerFrame.id : null,
+      points: [[0, 0]],
+      pressures: simulatePressure ? [] : [event.pressure],
     });
     });
 
 
+    this.scene.insertElement(element);
+
     this.setState((prevState) => {
     this.setState((prevState) => {
       const nextSelectedElementIds = {
       const nextSelectedElementIds = {
         ...prevState.selectedElementIds,
         ...prevState.selectedElementIds,
@@ -6964,21 +7006,12 @@ class App extends React.Component<AppProps, AppState> {
       };
       };
     });
     });
 
 
-    const pressures = element.simulatePressure
-      ? element.pressures
-      : [...element.pressures, event.pressure];
-
-    mutateElement(element, {
-      points: [[0, 0]],
-      pressures,
-    });
-
     const boundElement = getHoveredElementForBinding(
     const boundElement = getHoveredElementForBinding(
       pointerDownState.origin,
       pointerDownState.origin,
       this.scene.getNonDeletedElements(),
       this.scene.getNonDeletedElements(),
       this.scene.getNonDeletedElementsMap(),
       this.scene.getNonDeletedElementsMap(),
     );
     );
-    this.scene.insertElement(element);
+
     this.setState({
     this.setState({
       newElement: element,
       newElement: element,
       startBoundElement: boundElement,
       startBoundElement: boundElement,
@@ -7279,7 +7312,6 @@ class App extends React.Component<AppProps, AppState> {
       this.scene.insertElement(element);
       this.scene.insertElement(element);
       this.setState({
       this.setState({
         newElement: element,
         newElement: element,
-        editingElement: element,
         startBoundElement: boundElement,
         startBoundElement: boundElement,
         suggestedBindings: [],
         suggestedBindings: [],
       });
       });
@@ -7671,11 +7703,11 @@ class App extends React.Component<AppProps, AppState> {
 
 
         // prevent dragging even if we're no longer holding cmd/ctrl otherwise
         // prevent dragging even if we're no longer holding cmd/ctrl otherwise
         // it would have weird results (stuff jumping all over the screen)
         // it would have weird results (stuff jumping all over the screen)
-        // Checking for editingElement to avoid jump while editing on mobile #6503
+        // Checking for editingTextElement to avoid jump while editing on mobile #6503
         if (
         if (
           selectedElements.length > 0 &&
           selectedElements.length > 0 &&
           !pointerDownState.withCmdOrCtrl &&
           !pointerDownState.withCmdOrCtrl &&
-          !this.state.editingElement &&
+          !this.state.editingTextElement &&
           this.state.activeEmbeddable?.state !== "active"
           this.state.activeEmbeddable?.state !== "active"
         ) {
         ) {
           const dragOffset = {
           const dragOffset = {
@@ -7870,9 +7902,17 @@ class App extends React.Component<AppProps, AppState> {
               ? newElement.pressures
               ? newElement.pressures
               : [...newElement.pressures, event.pressure];
               : [...newElement.pressures, event.pressure];
 
 
-            mutateElement(newElement, {
-              points: [...points, [dx, dy]],
-              pressures,
+            mutateElement(
+              newElement,
+              {
+                points: [...points, [dx, dy]],
+                pressures,
+              },
+              false,
+            );
+
+            this.setState({
+              newElement,
             });
             });
           }
           }
         } else if (isLinearElement(newElement)) {
         } else if (isLinearElement(newElement)) {
@@ -7891,9 +7931,13 @@ class App extends React.Component<AppProps, AppState> {
           }
           }
 
 
           if (points.length === 1) {
           if (points.length === 1) {
-            mutateElement(newElement, {
-              points: [...points, [dx, dy]],
-            });
+            mutateElement(
+              newElement,
+              {
+                points: [...points, [dx, dy]],
+              },
+              false,
+            );
           } else if (points.length > 1 && isElbowArrow(newElement)) {
           } else if (points.length > 1 && isElbowArrow(newElement)) {
             mutateElbowArrow(
             mutateElbowArrow(
               newElement,
               newElement,
@@ -7903,14 +7947,23 @@ class App extends React.Component<AppProps, AppState> {
               undefined,
               undefined,
               {
               {
                 isDragging: true,
                 isDragging: true,
+                informMutation: false,
               },
               },
             );
             );
           } else if (points.length === 2) {
           } else if (points.length === 2) {
-            mutateElement(newElement, {
-              points: [...points.slice(0, -1), [dx, dy]],
-            });
+            mutateElement(
+              newElement,
+              {
+                points: [...points.slice(0, -1), [dx, dy]],
+              },
+              false,
+            );
           }
           }
 
 
+          this.setState({
+            newElement,
+          });
+
           if (isBindingElement(newElement, false)) {
           if (isBindingElement(newElement, false)) {
             // When creating a linear element by dragging
             // When creating a linear element by dragging
             this.maybeSuggestBindingsForLinearElementAtCoords(
             this.maybeSuggestBindingsForLinearElementAtCoords(
@@ -7922,7 +7975,7 @@ class App extends React.Component<AppProps, AppState> {
         } else {
         } else {
           pointerDownState.lastCoords.x = pointerCoords.x;
           pointerDownState.lastCoords.x = pointerCoords.x;
           pointerDownState.lastCoords.y = pointerCoords.y;
           pointerDownState.lastCoords.y = pointerCoords.y;
-          this.maybeDragNewGenericElement(pointerDownState, event);
+          this.maybeDragNewGenericElement(pointerDownState, event, false);
         }
         }
       }
       }
 
 
@@ -8080,12 +8133,6 @@ class App extends React.Component<AppProps, AppState> {
         frameToHighlight: null,
         frameToHighlight: null,
         elementsToHighlight: null,
         elementsToHighlight: null,
         cursorButton: "up",
         cursorButton: "up",
-        // text elements are reset on finalize, and resetting on pointerup
-        // may cause issues with double taps
-        editingElement:
-          multiElement || isTextElement(this.state.editingElement)
-            ? this.state.editingElement
-            : null,
         snapLines: updateStable(prevState.snapLines, []),
         snapLines: updateStable(prevState.snapLines, []),
         originSnapOffset: null,
         originSnapOffset: null,
       }));
       }));
@@ -8270,7 +8317,7 @@ class App extends React.Component<AppProps, AppState> {
           });
           });
           this.setState({
           this.setState({
             multiElement: newElement,
             multiElement: newElement,
-            editingElement: this.state.newElement,
+            newElement,
           });
           });
         } else if (pointerDownState.drag.hasOccurred && !multiElement) {
         } else if (pointerDownState.drag.hasOccurred && !multiElement) {
           if (
           if (
@@ -8307,6 +8354,8 @@ class App extends React.Component<AppProps, AppState> {
               newElement: null,
               newElement: null,
             }));
             }));
           }
           }
+          // so that the scene gets rendered again to display the newly drawn linear as well
+          this.scene.triggerUpdate();
         }
         }
         return;
         return;
       }
       }
@@ -8371,6 +8420,8 @@ class App extends React.Component<AppProps, AppState> {
 
 
       if (newElement) {
       if (newElement) {
         mutateElement(newElement, getNormalizedDimensions(newElement));
         mutateElement(newElement, getNormalizedDimensions(newElement));
+        // the above does not guarantee the scene to be rendered again, hence the trigger below
+        this.scene.triggerUpdate();
       }
       }
 
 
       if (pointerDownState.drag.hasOccurred) {
       if (pointerDownState.drag.hasOccurred) {
@@ -9179,7 +9230,7 @@ class App extends React.Component<AppProps, AppState> {
       this.setState(
       this.setState(
         {
         {
           pendingImageElementId: null,
           pendingImageElementId: null,
-          editingElement: null,
+          newElement: null,
           activeTool: updateActiveTool(this.state, { type: "selection" }),
           activeTool: updateActiveTool(this.state, { type: "selection" }),
         },
         },
         () => {
         () => {
@@ -9675,23 +9726,25 @@ class App extends React.Component<AppProps, AppState> {
   private maybeDragNewGenericElement = (
   private maybeDragNewGenericElement = (
     pointerDownState: PointerDownState,
     pointerDownState: PointerDownState,
     event: MouseEvent | KeyboardEvent,
     event: MouseEvent | KeyboardEvent,
+    informMutation = true,
   ): void => {
   ): void => {
     const selectionElement = this.state.selectionElement;
     const selectionElement = this.state.selectionElement;
     const pointerCoords = pointerDownState.lastCoords;
     const pointerCoords = pointerDownState.lastCoords;
     if (selectionElement && this.state.activeTool.type !== "eraser") {
     if (selectionElement && this.state.activeTool.type !== "eraser") {
-      dragNewElement(
-        selectionElement,
-        this.state.activeTool.type,
-        pointerDownState.origin.x,
-        pointerDownState.origin.y,
-        pointerCoords.x,
-        pointerCoords.y,
-        distance(pointerDownState.origin.x, pointerCoords.x),
-        distance(pointerDownState.origin.y, pointerCoords.y),
-        shouldMaintainAspectRatio(event),
-        shouldResizeFromCenter(event),
-        this.state.zoom.value,
-      );
+      dragNewElement({
+        newElement: selectionElement,
+        elementType: this.state.activeTool.type,
+        originX: pointerDownState.origin.x,
+        originY: pointerDownState.origin.y,
+        x: pointerCoords.x,
+        y: pointerCoords.y,
+        width: distance(pointerDownState.origin.x, pointerCoords.x),
+        height: distance(pointerDownState.origin.y, pointerCoords.y),
+        shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
+        shouldResizeFromCenter: shouldResizeFromCenter(event),
+        zoom: this.state.zoom.value,
+        informMutation,
+      });
       return;
       return;
     }
     }
 
 
@@ -9740,23 +9793,28 @@ class App extends React.Component<AppProps, AppState> {
       snapLines,
       snapLines,
     });
     });
 
 
-    dragNewElement(
+    dragNewElement({
       newElement,
       newElement,
-      this.state.activeTool.type,
-      pointerDownState.originInGrid.x,
-      pointerDownState.originInGrid.y,
-      gridX,
-      gridY,
-      distance(pointerDownState.originInGrid.x, gridX),
-      distance(pointerDownState.originInGrid.y, gridY),
-      isImageElement(newElement)
+      elementType: this.state.activeTool.type,
+      originX: pointerDownState.originInGrid.x,
+      originY: pointerDownState.originInGrid.y,
+      x: gridX,
+      y: gridY,
+      width: distance(pointerDownState.originInGrid.x, gridX),
+      height: distance(pointerDownState.originInGrid.y, gridY),
+      shouldMaintainAspectRatio: isImageElement(newElement)
         ? !shouldMaintainAspectRatio(event)
         ? !shouldMaintainAspectRatio(event)
         : shouldMaintainAspectRatio(event),
         : shouldMaintainAspectRatio(event),
-      shouldResizeFromCenter(event),
-      this.state.zoom.value,
-      aspectRatio,
-      this.state.originSnapOffset,
-    );
+      shouldResizeFromCenter: shouldResizeFromCenter(event),
+      zoom: this.state.zoom.value,
+      widthAspectRatio: aspectRatio,
+      originOffset: this.state.originSnapOffset,
+      informMutation,
+    });
+
+    this.setState({
+      newElement,
+    });
 
 
     // highlight elements that are to be added to frames on frames creation
     // highlight elements that are to be added to frames on frames creation
     if (
     if (

+ 2 - 2
packages/excalidraw/components/HintViewer.tsx

@@ -87,7 +87,7 @@ const getHints = ({
     return t("hints.text_selected");
     return t("hints.text_selected");
   }
   }
 
 
-  if (appState.editingElement && isTextElement(appState.editingElement)) {
+  if (appState.editingTextElement) {
     return t("hints.text_editing");
     return t("hints.text_editing");
   }
   }
 
 
@@ -95,7 +95,7 @@ const getHints = ({
     if (
     if (
       appState.selectionElement &&
       appState.selectionElement &&
       !selectedElements.length &&
       !selectedElements.length &&
-      !appState.editingElement &&
+      !appState.editingTextElement &&
       !appState.editingLinearElement
       !appState.editingLinearElement
     ) {
     ) {
       return t("hints.deepBoxSelect");
       return t("hints.deepBoxSelect");

+ 1 - 1
packages/excalidraw/components/canvases/InteractiveCanvas.tsx

@@ -202,7 +202,7 @@ const getRelevantAppStateProps = (
   activeEmbeddable: appState.activeEmbeddable,
   activeEmbeddable: appState.activeEmbeddable,
   snapLines: appState.snapLines,
   snapLines: appState.snapLines,
   zenModeEnabled: appState.zenModeEnabled,
   zenModeEnabled: appState.zenModeEnabled,
-  editingElement: appState.editingElement,
+  editingTextElement: appState.editingTextElement,
 });
 });
 
 
 const areEqual = (
 const areEqual = (

+ 56 - 0
packages/excalidraw/components/canvases/NewElementCanvas.tsx

@@ -0,0 +1,56 @@
+import { useEffect, useRef } from "react";
+import type { NonDeletedSceneElementsMap } from "../../element/types";
+import type { AppState } from "../../types";
+import type {
+  RenderableElementsMap,
+  StaticCanvasRenderConfig,
+} from "../../scene/types";
+import type { RoughCanvas } from "roughjs/bin/canvas";
+import { renderNewElementScene } from "../../renderer/renderNewElementScene";
+import { isRenderThrottlingEnabled } from "../../reactUtils";
+
+interface NewElementCanvasProps {
+  appState: AppState;
+  elementsMap: RenderableElementsMap;
+  allElementsMap: NonDeletedSceneElementsMap;
+  scale: number;
+  rc: RoughCanvas;
+  renderConfig: StaticCanvasRenderConfig;
+}
+
+const NewElementCanvas = (props: NewElementCanvasProps) => {
+  const canvasRef = useRef<HTMLCanvasElement | null>(null);
+  useEffect(() => {
+    if (!canvasRef.current) {
+      return;
+    }
+    renderNewElementScene(
+      {
+        canvas: canvasRef.current,
+        scale: props.scale,
+        newElement: props.appState.newElement,
+        elementsMap: props.elementsMap,
+        allElementsMap: props.allElementsMap,
+        rc: props.rc,
+        renderConfig: props.renderConfig,
+        appState: props.appState,
+      },
+      isRenderThrottlingEnabled(),
+    );
+  });
+
+  return (
+    <canvas
+      className="excalidraw__canvas"
+      style={{
+        width: props.appState.width,
+        height: props.appState.height,
+      }}
+      width={props.appState.width * props.scale}
+      height={props.appState.height * props.scale}
+      ref={canvasRef}
+    />
+  );
+};
+
+export default NewElementCanvas;

+ 1 - 1
packages/excalidraw/data/reconcile.ts

@@ -24,7 +24,7 @@ const shouldDiscardRemoteElement = (
   if (
   if (
     local &&
     local &&
     // local element is being edited
     // local element is being edited
-    (local.id === localAppState.editingElement?.id ||
+    (local.id === localAppState.editingTextElement?.id ||
       local.id === localAppState.resizingElement?.id ||
       local.id === localAppState.resizingElement?.id ||
       local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
       local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
       // local element is newer
       // local element is newer

+ 43 - 23
packages/excalidraw/element/dragElements.ts

@@ -159,26 +159,42 @@ export const getDragOffsetXY = (
   return [x - x1, y - y1];
   return [x - x1, y - y1];
 };
 };
 
 
-export const dragNewElement = (
-  newElement: NonDeletedExcalidrawElement,
-  elementType: AppState["activeTool"]["type"],
-  originX: number,
-  originY: number,
-  x: number,
-  y: number,
-  width: number,
-  height: number,
-  shouldMaintainAspectRatio: boolean,
-  shouldResizeFromCenter: boolean,
-  zoom: NormalizedZoomValue,
+export const dragNewElement = ({
+  newElement,
+  elementType,
+  originX,
+  originY,
+  x,
+  y,
+  width,
+  height,
+  shouldMaintainAspectRatio,
+  shouldResizeFromCenter,
+  zoom,
+  widthAspectRatio = null,
+  originOffset = null,
+  informMutation = true,
+}: {
+  newElement: NonDeletedExcalidrawElement;
+  elementType: AppState["activeTool"]["type"];
+  originX: number;
+  originY: number;
+  x: number;
+  y: number;
+  width: number;
+  height: number;
+  shouldMaintainAspectRatio: boolean;
+  shouldResizeFromCenter: boolean;
+  zoom: NormalizedZoomValue;
   /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
   /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
       true */
       true */
-  widthAspectRatio?: number | null,
-  originOffset: {
+  widthAspectRatio?: number | null;
+  originOffset?: {
     x: number;
     x: number;
     y: number;
     y: number;
-  } | null = null,
-) => {
+  } | null;
+  informMutation?: boolean;
+}) => {
   if (shouldMaintainAspectRatio && newElement.type !== "selection") {
   if (shouldMaintainAspectRatio && newElement.type !== "selection") {
     if (widthAspectRatio) {
     if (widthAspectRatio) {
       height = width / widthAspectRatio;
       height = width / widthAspectRatio;
@@ -242,12 +258,16 @@ export const dragNewElement = (
   }
   }
 
 
   if (width !== 0 && height !== 0) {
   if (width !== 0 && height !== 0) {
-    mutateElement(newElement, {
-      x: newX + (originOffset?.x ?? 0),
-      y: newY + (originOffset?.y ?? 0),
-      width,
-      height,
-      ...textAutoResize,
-    });
+    mutateElement(
+      newElement,
+      {
+        x: newX + (originOffset?.x ?? 0),
+        y: newY + (originOffset?.y ?? 0),
+        width,
+        height,
+        ...textAutoResize,
+      },
+      informMutation,
+    );
   }
   }
 };
 };

+ 2 - 1
packages/excalidraw/element/newElement.ts

@@ -375,12 +375,13 @@ export const newFreeDrawElement = (
     type: "freedraw";
     type: "freedraw";
     points?: ExcalidrawFreeDrawElement["points"];
     points?: ExcalidrawFreeDrawElement["points"];
     simulatePressure: boolean;
     simulatePressure: boolean;
+    pressures?: ExcalidrawFreeDrawElement["pressures"];
   } & ElementConstructorOpts,
   } & ElementConstructorOpts,
 ): NonDeleted<ExcalidrawFreeDrawElement> => {
 ): NonDeleted<ExcalidrawFreeDrawElement> => {
   return {
   return {
     ..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
     ..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
     points: opts.points || [],
     points: opts.points || [],
-    pressures: [],
+    pressures: opts.pressures || [],
     simulatePressure: opts.simulatePressure,
     simulatePressure: opts.simulatePressure,
     lastCommittedPoint: null,
     lastCommittedPoint: null,
   };
   };

+ 1 - 1
packages/excalidraw/element/showSelectedShapeActions.ts

@@ -9,7 +9,7 @@ export const showSelectedShapeActions = (
   Boolean(
   Boolean(
     !appState.viewModeEnabled &&
     !appState.viewModeEnabled &&
       ((appState.activeTool.type !== "custom" &&
       ((appState.activeTool.type !== "custom" &&
-        (appState.editingElement ||
+        (appState.editingTextElement ||
           (appState.activeTool.type !== "selection" &&
           (appState.activeTool.type !== "selection" &&
             appState.activeTool.type !== "eraser" &&
             appState.activeTool.type !== "eraser" &&
             appState.activeTool.type !== "hand" &&
             appState.activeTool.type !== "hand" &&

+ 9 - 9
packages/excalidraw/element/textWysiwyg.test.tsx

@@ -61,9 +61,9 @@ describe("textWysiwyg", () => {
 
 
       Keyboard.keyPress(KEYS.ENTER);
       Keyboard.keyPress(KEYS.ENTER);
 
 
-      expect(h.state.editingElement?.id).toBe(text.id);
+      expect(h.state.editingTextElement?.id).toBe(text.id);
       expect(
       expect(
-        (h.state.editingElement as ExcalidrawTextElement).containerId,
+        (h.state.editingTextElement as ExcalidrawTextElement).containerId,
       ).toBe(null);
       ).toBe(null);
     });
     });
 
 
@@ -105,7 +105,7 @@ describe("textWysiwyg", () => {
 
 
       Keyboard.keyPress(KEYS.ENTER);
       Keyboard.keyPress(KEYS.ENTER);
 
 
-      expect(h.state.editingElement?.id).toBe(boundText2.id);
+      expect(h.state.editingTextElement?.id).toBe(boundText2.id);
     });
     });
 
 
     it("should not create bound text on ENTER if text exists at container center", () => {
     it("should not create bound text on ENTER if text exists at container center", () => {
@@ -133,7 +133,7 @@ describe("textWysiwyg", () => {
 
 
       Keyboard.keyPress(KEYS.ENTER);
       Keyboard.keyPress(KEYS.ENTER);
 
 
-      expect(h.state.editingElement?.id).toBe(text.id);
+      expect(h.state.editingTextElement?.id).toBe(text.id);
     });
     });
 
 
     it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
     it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
@@ -174,7 +174,7 @@ describe("textWysiwyg", () => {
 
 
       Keyboard.keyPress(KEYS.ENTER);
       Keyboard.keyPress(KEYS.ENTER);
 
 
-      expect(h.state.editingElement?.id).toBe(boundText.id);
+      expect(h.state.editingTextElement?.id).toBe(boundText.id);
     });
     });
 
 
     it("should edit text under cursor when clicked with text tool", async () => {
     it("should edit text under cursor when clicked with text tool", async () => {
@@ -195,7 +195,7 @@ describe("textWysiwyg", () => {
       const editor = await getTextEditor(textEditorSelector, false);
       const editor = await getTextEditor(textEditorSelector, false);
 
 
       expect(editor).not.toBe(null);
       expect(editor).not.toBe(null);
-      expect(h.state.editingElement?.id).toBe(text.id);
+      expect(h.state.editingTextElement?.id).toBe(text.id);
       expect(h.elements.length).toBe(1);
       expect(h.elements.length).toBe(1);
     });
     });
 
 
@@ -217,7 +217,7 @@ describe("textWysiwyg", () => {
       const editor = await getTextEditor(textEditorSelector, false);
       const editor = await getTextEditor(textEditorSelector, false);
 
 
       expect(editor).not.toBe(null);
       expect(editor).not.toBe(null);
-      expect(h.state.editingElement?.id).toBe(text.id);
+      expect(h.state.editingTextElement?.id).toBe(text.id);
       expect(h.elements.length).toBe(1);
       expect(h.elements.length).toBe(1);
     });
     });
 
 
@@ -286,7 +286,7 @@ describe("textWysiwyg", () => {
       mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
       mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
       const editor = await getTextEditor(textEditorSelector);
       const editor = await getTextEditor(textEditorSelector);
       expect(editor).not.toBe(null);
       expect(editor).not.toBe(null);
-      expect(h.state.editingElement?.id).toBe(text.id);
+      expect(h.state.editingTextElement?.id).toBe(text.id);
       expect(h.elements.length).toBe(1);
       expect(h.elements.length).toBe(1);
 
 
       const nextText = `${wrappedText} is great!`;
       const nextText = `${wrappedText} is great!`;
@@ -881,7 +881,7 @@ describe("textWysiwyg", () => {
 
 
       expect(await getTextEditor(textEditorSelector, false)).toBe(null);
       expect(await getTextEditor(textEditorSelector, false)).toBe(null);
 
 
-      expect(h.state.editingElement).toBe(null);
+      expect(h.state.editingTextElement).toBe(null);
 
 
       expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont);
       expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont);
 
 

+ 6 - 3
packages/excalidraw/renderer/interactiveScene.ts

@@ -680,8 +680,11 @@ const _renderInteractiveScene = ({
     }
     }
   }
   }
 
 
-  if (appState.editingElement && isTextElement(appState.editingElement)) {
-    const textElement = allElementsMap.get(appState.editingElement.id) as
+  if (
+    appState.editingTextElement &&
+    isTextElement(appState.editingTextElement)
+  ) {
+    const textElement = allElementsMap.get(appState.editingTextElement.id) as
       | ExcalidrawTextElement
       | ExcalidrawTextElement
       | undefined;
       | undefined;
     if (textElement && !textElement.autoResize) {
     if (textElement && !textElement.autoResize) {
@@ -894,7 +897,7 @@ const _renderInteractiveScene = ({
         !appState.viewModeEnabled &&
         !appState.viewModeEnabled &&
         showBoundingBox &&
         showBoundingBox &&
         // do not show transform handles when text is being edited
         // do not show transform handles when text is being edited
-        !isTextElement(appState.editingElement)
+        !isTextElement(appState.editingTextElement)
       ) {
       ) {
         renderTransformHandles(
         renderTransformHandles(
           context,
           context,

+ 66 - 0
packages/excalidraw/renderer/renderNewElementScene.ts

@@ -0,0 +1,66 @@
+import type { NewElementSceneRenderConfig } from "../scene/types";
+import { throttleRAF } from "../utils";
+import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
+import { renderElement } from "./renderElement";
+
+const _renderNewElementScene = ({
+  canvas,
+  rc,
+  newElement,
+  elementsMap,
+  allElementsMap,
+  scale,
+  appState,
+  renderConfig,
+}: NewElementSceneRenderConfig) => {
+  if (canvas) {
+    const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
+      canvas,
+      scale,
+    );
+
+    const context = bootstrapCanvas({
+      canvas,
+      scale,
+      normalizedWidth,
+      normalizedHeight,
+    });
+
+    // Apply zoom
+    context.save();
+    context.scale(appState.zoom.value, appState.zoom.value);
+
+    if (newElement && newElement.type !== "selection") {
+      renderElement(
+        newElement,
+        elementsMap,
+        allElementsMap,
+        rc,
+        context,
+        renderConfig,
+        appState,
+      );
+    } else {
+      context.clearRect(0, 0, normalizedWidth, normalizedHeight);
+    }
+  }
+};
+
+export const renderNewElementSceneThrottled = throttleRAF(
+  (config: NewElementSceneRenderConfig) => {
+    _renderNewElementScene(config);
+  },
+  { trailing: true },
+);
+
+export const renderNewElementScene = (
+  renderConfig: NewElementSceneRenderConfig,
+  throttle?: boolean,
+) => {
+  if (throttle) {
+    renderNewElementSceneThrottled(renderConfig);
+    return;
+  }
+
+  _renderNewElementScene(renderConfig);
+};

+ 20 - 8
packages/excalidraw/scene/Renderer.ts

@@ -1,6 +1,7 @@
 import { isElementInViewport } from "../element/sizeHelpers";
 import { isElementInViewport } from "../element/sizeHelpers";
 import { isImageElement } from "../element/typeChecks";
 import { isImageElement } from "../element/typeChecks";
 import type {
 import type {
+  ExcalidrawElement,
   NonDeletedElementsMap,
   NonDeletedElementsMap,
   NonDeletedExcalidrawElement,
   NonDeletedExcalidrawElement,
 } from "../element/types";
 } from "../element/types";
@@ -64,11 +65,13 @@ export class Renderer {
 
 
     const getRenderableElements = ({
     const getRenderableElements = ({
       elements,
       elements,
-      editingElement,
+      editingTextElement,
+      newElementId,
       pendingImageElementId,
       pendingImageElementId,
     }: {
     }: {
       elements: readonly NonDeletedExcalidrawElement[];
       elements: readonly NonDeletedExcalidrawElement[];
-      editingElement: AppState["editingElement"];
+      editingTextElement: AppState["editingTextElement"];
+      newElementId: ExcalidrawElement["id"] | undefined;
       pendingImageElementId: AppState["pendingImageElementId"];
       pendingImageElementId: AppState["pendingImageElementId"];
     }) => {
     }) => {
       const elementsMap = toBrandedType<RenderableElementsMap>(new Map());
       const elementsMap = toBrandedType<RenderableElementsMap>(new Map());
@@ -83,12 +86,16 @@ export class Renderer {
           }
           }
         }
         }
 
 
+        if (newElementId === element.id) {
+          continue;
+        }
+
         // we don't want to render text element that's being currently edited
         // we don't want to render text element that's being currently edited
         // (it's rendered on remote only)
         // (it's rendered on remote only)
         if (
         if (
-          !editingElement ||
-          editingElement.type !== "text" ||
-          element.id !== editingElement.id
+          !editingTextElement ||
+          editingTextElement.type !== "text" ||
+          element.id !== editingTextElement.id
         ) {
         ) {
           elementsMap.set(element.id, element);
           elementsMap.set(element.id, element);
         }
         }
@@ -105,7 +112,8 @@ export class Renderer {
         scrollY,
         scrollY,
         height,
         height,
         width,
         width,
-        editingElement,
+        editingTextElement,
+        newElementId,
         pendingImageElementId,
         pendingImageElementId,
         // cache-invalidation nonce
         // cache-invalidation nonce
         sceneNonce: _sceneNonce,
         sceneNonce: _sceneNonce,
@@ -117,7 +125,10 @@ export class Renderer {
         scrollY: AppState["scrollY"];
         scrollY: AppState["scrollY"];
         height: AppState["height"];
         height: AppState["height"];
         width: AppState["width"];
         width: AppState["width"];
-        editingElement: AppState["editingElement"];
+        editingTextElement: AppState["editingTextElement"];
+        /** note: first render of newElement will always bust the cache
+         * (we'd have to prefilter elements outside of this function) */
+        newElementId: ExcalidrawElement["id"] | undefined;
         pendingImageElementId: AppState["pendingImageElementId"];
         pendingImageElementId: AppState["pendingImageElementId"];
         sceneNonce: ReturnType<InstanceType<typeof Scene>["getSceneNonce"]>;
         sceneNonce: ReturnType<InstanceType<typeof Scene>["getSceneNonce"]>;
       }) => {
       }) => {
@@ -125,7 +136,8 @@ export class Renderer {
 
 
         const elementsMap = getRenderableElements({
         const elementsMap = getRenderableElements({
           elements,
           elements,
-          editingElement,
+          editingTextElement,
+          newElementId,
           pendingImageElementId,
           pendingImageElementId,
         });
         });
 
 

+ 8 - 3
packages/excalidraw/scene/selection.ts

@@ -218,10 +218,15 @@ export const getSelectedElements = (
 
 
 export const getTargetElements = (
 export const getTargetElements = (
   elements: ElementsMapOrArray,
   elements: ElementsMapOrArray,
-  appState: Pick<AppState, "selectedElementIds" | "editingElement">,
+  appState: Pick<
+    AppState,
+    "selectedElementIds" | "editingTextElement" | "newElement"
+  >,
 ) =>
 ) =>
-  appState.editingElement
-    ? [appState.editingElement]
+  appState.editingTextElement
+    ? [appState.editingTextElement]
+    : appState.newElement
+    ? [appState.newElement]
     : getSelectedElements(elements, appState, {
     : getSelectedElements(elements, appState, {
         includeBoundTextElement: true,
         includeBoundTextElement: true,
       });
       });

+ 11 - 0
packages/excalidraw/scene/types.ts

@@ -92,6 +92,17 @@ export type InteractiveSceneRenderConfig = {
   callback: (data: RenderInteractiveSceneCallback) => void;
   callback: (data: RenderInteractiveSceneCallback) => void;
 };
 };
 
 
+export type NewElementSceneRenderConfig = {
+  canvas: HTMLCanvasElement | null;
+  rc: RoughCanvas;
+  newElement: ExcalidrawElement | null;
+  elementsMap: RenderableElementsMap;
+  allElementsMap: NonDeletedSceneElementsMap;
+  scale: number;
+  appState: AppState;
+  renderConfig: StaticCanvasRenderConfig;
+};
+
 export type SceneScroll = {
 export type SceneScroll = {
   scrollX: number;
   scrollX: number;
   scrollY: number;
   scrollY: number;

+ 31 - 31
packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap

@@ -812,10 +812,10 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -1017,10 +1017,10 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -1201,7 +1201,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of elements 1`] = `1`;
 exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of elements 1`] = `1`;
 
 
-exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `6`;
+exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `5`;
 
 
 exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] appState 1`] = `
 {
 {
@@ -1232,10 +1232,10 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -1531,7 +1531,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `12`;
+exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `10`;
 
 
 exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] appState 1`] = `
 {
 {
@@ -1562,10 +1562,10 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -1861,7 +1861,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `12`;
+exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `10`;
 
 
 exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] appState 1`] = `
 {
 {
@@ -1892,10 +1892,10 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -2076,7 +2076,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of elements 1`] = `1`;
 exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of elements 1`] = `1`;
 
 
-exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `6`;
+exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `5`;
 
 
 exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] appState 1`] = `
 {
 {
@@ -2107,10 +2107,10 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -2315,7 +2315,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of elements 1`] = `1`;
 exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of elements 1`] = `1`;
 
 
-exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `7`;
+exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `6`;
 
 
 exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] appState 1`] = `
 {
 {
@@ -2346,10 +2346,10 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -2615,7 +2615,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `7`;
+exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `6`;
 
 
 exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] appState 1`] = `
 {
 {
@@ -2646,10 +2646,10 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -2983,7 +2983,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `12`;
+exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `10`;
 
 
 exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] appState 1`] = `
 {
 {
@@ -3014,10 +3014,10 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -3457,7 +3457,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `18`;
+exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `16`;
 
 
 exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] appState 1`] = `
 {
 {
@@ -3488,10 +3488,10 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -3779,7 +3779,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `11`;
+exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `9`;
 
 
 exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] appState 1`] = `
 {
 {
@@ -3810,10 +3810,10 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -4101,7 +4101,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `11`;
+exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `9`;
 
 
 exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] appState 1`] = `
 exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] appState 1`] = `
 {
 {
@@ -4132,10 +4132,10 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -4503,7 +4503,7 @@ History {
 
 
 exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `13`;
+exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `11`;
 
 
 exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] appState 1`] = `
 exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] appState 1`] = `
 {
 {
@@ -5317,10 +5317,10 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -5629,7 +5629,7 @@ History {
 
 
 exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `12`;
+exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `10`;
 
 
 exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] appState 1`] = `
 exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] appState 1`] = `
 {
 {
@@ -6443,10 +6443,10 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -6801,7 +6801,7 @@ History {
 
 
 exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of elements 1`] = `2`;
 exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of elements 1`] = `2`;
 
 
-exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `13`;
+exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `11`;
 
 
 exports[`contextMenu element > shows context menu for canvas > [end of test] appState 1`] = `
 exports[`contextMenu element > shows context menu for canvas > [end of test] appState 1`] = `
 {
 {
@@ -7377,10 +7377,10 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -8288,10 +8288,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -9181,10 +9181,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,
@@ -9449,6 +9449,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] nu
 
 
 exports[`contextMenu element > shows context menu for element > [end of test] number of elements 2`] = `2`;
 exports[`contextMenu element > shows context menu for element > [end of test] number of elements 2`] = `2`;
 
 
-exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `6`;
+exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `5`;
 
 
 exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `6`;
 exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `6`;

+ 9 - 9
packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap

@@ -1,8 +1,8 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 3`] = `1`;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 2`] = `
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 4`] = `
 {
 {
   "angle": 0,
   "angle": 0,
   "backgroundColor": "transparent",
   "backgroundColor": "transparent",
@@ -52,9 +52,9 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 }
 `;
 `;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 3`] = `1`;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 2`] = `
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 4`] = `
 {
 {
   "angle": 0,
   "angle": 0,
   "backgroundColor": "transparent",
   "backgroundColor": "transparent",
@@ -88,9 +88,9 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 }
 `;
 `;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 3`] = `1`;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 2`] = `
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 4`] = `
 {
 {
   "angle": 0,
   "angle": 0,
   "backgroundColor": "transparent",
   "backgroundColor": "transparent",
@@ -124,7 +124,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 }
 `;
 `;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > line 1`] = `
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > line 3`] = `
 {
 {
   "angle": 0,
   "angle": 0,
   "backgroundColor": "transparent",
   "backgroundColor": "transparent",
@@ -173,9 +173,9 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 }
 `;
 `;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 3`] = `1`;
 
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 2`] = `
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 4`] = `
 {
 {
   "angle": 0,
   "angle": 0,
   "backgroundColor": "transparent",
   "backgroundColor": "transparent",

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 94 - 94
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap


+ 1 - 1
packages/excalidraw/tests/__snapshots__/move.test.tsx.snap

@@ -134,7 +134,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
   "type": "rectangle",
   "type": "rectangle",
   "updated": 1,
   "updated": 1,
   "version": 4,
   "version": 4,
-  "versionNonce": 760410951,
+  "versionNonce": 1723083209,
   "width": 100,
   "width": 100,
   "x": 0,
   "x": 0,
   "y": 0,
   "y": 0,

+ 2 - 2
packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap

@@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 8,
   "version": 8,
-  "versionNonce": 23633383,
+  "versionNonce": 1604849351,
   "width": 70,
   "width": 70,
   "x": 30,
   "x": 30,
   "y": 30,
   "y": 30,
@@ -106,7 +106,7 @@ exports[`multi point mode in linear elements > line 3`] = `
   "type": "line",
   "type": "line",
   "updated": 1,
   "updated": 1,
   "version": 8,
   "version": 8,
-  "versionNonce": 23633383,
+  "versionNonce": 1604849351,
   "width": 70,
   "width": 70,
   "x": 30,
   "x": 30,
   "y": 30,
   "y": 30,

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 95 - 95
packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap


+ 40 - 20
packages/excalidraw/tests/dragCreate.test.tsx

@@ -51,8 +51,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
-      expect(renderStaticScene).toHaveBeenCalledTimes(6);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
 
 
       expect(h.elements.length).toEqual(1);
       expect(h.elements.length).toEqual(1);
@@ -83,8 +85,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
-      expect(renderStaticScene).toHaveBeenCalledTimes(6);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
 
 
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
 
 
@@ -116,8 +120,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
-      expect(renderStaticScene).toHaveBeenCalledTimes(6);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
 
 
       expect(h.elements.length).toEqual(1);
       expect(h.elements.length).toEqual(1);
@@ -148,8 +154,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
-      expect(renderStaticScene).toHaveBeenCalledTimes(6);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
 
 
       expect(h.elements.length).toEqual(1);
       expect(h.elements.length).toEqual(1);
@@ -184,8 +192,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
-      expect(renderStaticScene).toHaveBeenCalledTimes(6);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
 
 
       expect(h.elements.length).toEqual(1);
       expect(h.elements.length).toEqual(1);
@@ -225,8 +235,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
-      expect(renderStaticScene).toHaveBeenCalledTimes(5);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
       expect(h.elements.length).toEqual(0);
       expect(h.elements.length).toEqual(0);
     });
     });
@@ -245,8 +257,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
-      expect(renderStaticScene).toHaveBeenCalledTimes(5);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
       expect(h.elements.length).toEqual(0);
       expect(h.elements.length).toEqual(0);
     });
     });
@@ -265,8 +279,10 @@ describe("Test dragCreate", () => {
       // finish (position does not matter)
       // finish (position does not matter)
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
-      expect(renderStaticScene).toHaveBeenCalledTimes(5);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `5`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
       expect(h.elements.length).toEqual(0);
       expect(h.elements.length).toEqual(0);
     });
     });
@@ -292,8 +308,10 @@ describe("Test dragCreate", () => {
         key: KEYS.ENTER,
         key: KEYS.ENTER,
       });
       });
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
-      expect(renderStaticScene).toHaveBeenCalledTimes(6);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `6`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
       expect(h.elements.length).toEqual(0);
       expect(h.elements.length).toEqual(0);
     });
     });
@@ -319,8 +337,10 @@ describe("Test dragCreate", () => {
         key: KEYS.ENTER,
         key: KEYS.ENTER,
       });
       });
 
 
-      expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
-      expect(renderStaticScene).toHaveBeenCalledTimes(6);
+      expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
+        `6`,
+      );
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
       expect(h.elements.length).toEqual(0);
       expect(h.elements.length).toEqual(0);
     });
     });

+ 10 - 10
packages/excalidraw/tests/elementLocking.test.tsx

@@ -232,8 +232,8 @@ describe("element locking", () => {
     API.setElements([container, text]);
     API.setElements([container, text]);
     API.setSelectedElements([container]);
     API.setSelectedElements([container]);
     Keyboard.keyPress(KEYS.ENTER);
     Keyboard.keyPress(KEYS.ENTER);
-    expect(h.state.editingElement?.id).not.toBe(text.id);
-    expect(h.state.editingElement?.id).toBe(h.elements[1].id);
+    expect(h.state.editingTextElement?.id).not.toBe(text.id);
+    expect(h.state.editingTextElement?.id).toBe(h.elements[1].id);
   });
   });
 
 
   it("should ignore locked text under cursor when clicked with text tool", () => {
   it("should ignore locked text under cursor when clicked with text tool", () => {
@@ -253,9 +253,9 @@ describe("element locking", () => {
       ".excalidraw-textEditorContainer > textarea",
       ".excalidraw-textEditorContainer > textarea",
     ) as HTMLTextAreaElement;
     ) as HTMLTextAreaElement;
     expect(editor).not.toBe(null);
     expect(editor).not.toBe(null);
-    expect(h.state.editingElement?.id).not.toBe(text.id);
+    expect(h.state.editingTextElement?.id).not.toBe(text.id);
     expect(h.elements.length).toBe(2);
     expect(h.elements.length).toBe(2);
-    expect(h.state.editingElement?.id).toBe(h.elements[1].id);
+    expect(h.state.editingTextElement?.id).toBe(h.elements[1].id);
   });
   });
 
 
   it("should ignore text under cursor when double-clicked with selection tool", () => {
   it("should ignore text under cursor when double-clicked with selection tool", () => {
@@ -275,9 +275,9 @@ describe("element locking", () => {
       ".excalidraw-textEditorContainer > textarea",
       ".excalidraw-textEditorContainer > textarea",
     ) as HTMLTextAreaElement;
     ) as HTMLTextAreaElement;
     expect(editor).not.toBe(null);
     expect(editor).not.toBe(null);
-    expect(h.state.editingElement?.id).not.toBe(text.id);
+    expect(h.state.editingTextElement?.id).not.toBe(text.id);
     expect(h.elements.length).toBe(2);
     expect(h.elements.length).toBe(2);
-    expect(h.state.editingElement?.id).toBe(h.elements[1].id);
+    expect(h.state.editingTextElement?.id).toBe(h.elements[1].id);
   });
   });
 
 
   it("locking should include bound text", () => {
   it("locking should include bound text", () => {
@@ -348,9 +348,9 @@ describe("element locking", () => {
       ".excalidraw-textEditorContainer > textarea",
       ".excalidraw-textEditorContainer > textarea",
     ) as HTMLTextAreaElement;
     ) as HTMLTextAreaElement;
     expect(editor).not.toBe(null);
     expect(editor).not.toBe(null);
-    expect(h.state.editingElement?.id).not.toBe(text.id);
+    expect(h.state.editingTextElement?.id).not.toBe(text.id);
     expect(h.elements.length).toBe(3);
     expect(h.elements.length).toBe(3);
-    expect(h.state.editingElement?.id).toBe(h.elements[2].id);
+    expect(h.state.editingTextElement?.id).toBe(h.elements[2].id);
   });
   });
 
 
   it("bound text shouldn't be editable via text tool", () => {
   it("bound text shouldn't be editable via text tool", () => {
@@ -382,8 +382,8 @@ describe("element locking", () => {
       ".excalidraw-textEditorContainer > textarea",
       ".excalidraw-textEditorContainer > textarea",
     ) as HTMLTextAreaElement;
     ) as HTMLTextAreaElement;
     expect(editor).not.toBe(null);
     expect(editor).not.toBe(null);
-    expect(h.state.editingElement?.id).not.toBe(text.id);
+    expect(h.state.editingTextElement?.id).not.toBe(text.id);
     expect(h.elements.length).toBe(3);
     expect(h.elements.length).toBe(3);
-    expect(h.state.editingElement?.id).toBe(h.elements[2].id);
+    expect(h.state.editingTextElement?.id).toBe(h.elements[2].id);
   });
   });
 });
 });

+ 5 - 5
packages/excalidraw/tests/history.test.tsx

@@ -753,7 +753,7 @@ describe("history", () => {
       expect(API.getRedoStack().length).toBe(0);
       expect(API.getRedoStack().length).toBe(0);
       expect(assertSelectedElements(h.elements[0]));
       expect(assertSelectedElements(h.elements[0]));
       expect(h.state.editingLinearElement).toBeNull();
       expect(h.state.editingLinearElement).toBeNull();
-      expect(h.state.selectedLinearElement).toBeNull();
+      expect(h.state.selectedLinearElement).not.toBeNull();
       expect(h.elements).toEqual([
       expect(h.elements).toEqual([
         expect.objectContaining({
         expect.objectContaining({
           isDeleted: false,
           isDeleted: false,
@@ -961,7 +961,7 @@ describe("history", () => {
       expect(API.getRedoStack().length).toBe(0);
       expect(API.getRedoStack().length).toBe(0);
       expect(assertSelectedElements(h.elements[0]));
       expect(assertSelectedElements(h.elements[0]));
       expect(h.state.editingLinearElement).toBeNull();
       expect(h.state.editingLinearElement).toBeNull();
-      expect(h.state.selectedLinearElement).toBeNull();
+      expect(h.state.selectedLinearElement).not.toBeNull();
       expect(h.elements).toEqual([
       expect(h.elements).toEqual([
         expect.objectContaining({
         expect.objectContaining({
           isDeleted: false,
           isDeleted: false,
@@ -2727,7 +2727,7 @@ describe("history", () => {
       expect(API.getUndoStack().length).toBe(4);
       expect(API.getUndoStack().length).toBe(4);
       expect(API.getRedoStack().length).toBe(0);
       expect(API.getRedoStack().length).toBe(0);
       expect(h.state.editingLinearElement).toBeNull();
       expect(h.state.editingLinearElement).toBeNull();
-      expect(h.state.selectedLinearElement).toBeNull();
+      expect(h.state.selectedLinearElement).not.toBeNull();
 
 
       // Simulate remote update
       // Simulate remote update
       API.updateScene({
       API.updateScene({
@@ -2740,8 +2740,8 @@ describe("history", () => {
       });
       });
 
 
       Keyboard.undo();
       Keyboard.undo();
-      expect(API.getUndoStack().length).toBe(0);
-      expect(API.getRedoStack().length).toBe(4);
+      expect(API.getUndoStack().length).toBe(1);
+      expect(API.getRedoStack().length).toBe(3);
       expect(h.state.editingLinearElement).toBeNull();
       expect(h.state.editingLinearElement).toBeNull();
       expect(h.state.selectedLinearElement).toBeNull();
       expect(h.state.selectedLinearElement).toBeNull();
 
 

+ 6 - 6
packages/excalidraw/tests/move.test.tsx

@@ -48,9 +48,9 @@ describe("move element", () => {
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
       expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
       expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-        `6`,
+        `5`,
       );
       );
-      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
       expect(h.elements.length).toEqual(1);
       expect(h.elements.length).toEqual(1);
       expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
       expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
@@ -96,9 +96,9 @@ describe("move element", () => {
     new Pointer("mouse").clickOn(rectB);
     new Pointer("mouse").clickOn(rectB);
 
 
     expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
     expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-      `19`,
+      `17`,
     );
     );
-    expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`16`);
+    expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`13`);
     expect(h.state.selectionElement).toBeNull();
     expect(h.state.selectionElement).toBeNull();
     expect(h.elements.length).toEqual(3);
     expect(h.elements.length).toEqual(3);
     expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
     expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
@@ -146,9 +146,9 @@ describe("duplicate element on move when ALT is clicked", () => {
       fireEvent.pointerUp(canvas);
       fireEvent.pointerUp(canvas);
 
 
       expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
       expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-        `6`,
+        `5`,
       );
       );
-      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
+      expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
       expect(h.state.selectionElement).toBeNull();
       expect(h.state.selectionElement).toBeNull();
       expect(h.elements.length).toEqual(1);
       expect(h.elements.length).toEqual(1);
       expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
       expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();

+ 4 - 4
packages/excalidraw/tests/multiPointCreate.test.tsx

@@ -115,8 +115,8 @@ describe("multi point mode in linear elements", () => {
       key: KEYS.ENTER,
       key: KEYS.ENTER,
     });
     });
 
 
-    expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
-    expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
+    expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
+    expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
     expect(h.elements.length).toEqual(1);
     expect(h.elements.length).toEqual(1);
 
 
     const element = h.elements[0] as ExcalidrawLinearElement;
     const element = h.elements[0] as ExcalidrawLinearElement;
@@ -158,8 +158,8 @@ describe("multi point mode in linear elements", () => {
     fireEvent.keyDown(document, {
     fireEvent.keyDown(document, {
       key: KEYS.ENTER,
       key: KEYS.ENTER,
     });
     });
-    expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
-    expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
+    expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
+    expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
     expect(h.elements.length).toEqual(1);
     expect(h.elements.length).toEqual(1);
 
 
     const element = h.elements[0] as ExcalidrawLinearElement;
     const element = h.elements[0] as ExcalidrawLinearElement;

+ 10 - 10
packages/excalidraw/tests/selection.test.tsx

@@ -314,8 +314,8 @@ describe("select single element on the scene", () => {
     fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
     fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
     fireEvent.pointerUp(canvas);
     fireEvent.pointerUp(canvas);
 
 
-    expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
-    expect(renderStaticScene).toHaveBeenCalledTimes(7);
+    expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
+    expect(renderStaticScene).toHaveBeenCalledTimes(6);
     expect(h.state.selectionElement).toBeNull();
     expect(h.state.selectionElement).toBeNull();
     expect(h.elements.length).toEqual(1);
     expect(h.elements.length).toEqual(1);
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
@@ -346,8 +346,8 @@ describe("select single element on the scene", () => {
     fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
     fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
     fireEvent.pointerUp(canvas);
     fireEvent.pointerUp(canvas);
 
 
-    expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
-    expect(renderStaticScene).toHaveBeenCalledTimes(7);
+    expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
+    expect(renderStaticScene).toHaveBeenCalledTimes(6);
     expect(h.state.selectionElement).toBeNull();
     expect(h.state.selectionElement).toBeNull();
     expect(h.elements.length).toEqual(1);
     expect(h.elements.length).toEqual(1);
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
@@ -378,8 +378,8 @@ describe("select single element on the scene", () => {
     fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
     fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
     fireEvent.pointerUp(canvas);
     fireEvent.pointerUp(canvas);
 
 
-    expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
-    expect(renderStaticScene).toHaveBeenCalledTimes(7);
+    expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
+    expect(renderStaticScene).toHaveBeenCalledTimes(6);
     expect(h.state.selectionElement).toBeNull();
     expect(h.state.selectionElement).toBeNull();
     expect(h.elements.length).toEqual(1);
     expect(h.elements.length).toEqual(1);
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
@@ -423,8 +423,8 @@ describe("select single element on the scene", () => {
     fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
     fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
     fireEvent.pointerUp(canvas);
     fireEvent.pointerUp(canvas);
 
 
-    expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
-    expect(renderStaticScene).toHaveBeenCalledTimes(7);
+    expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
+    expect(renderStaticScene).toHaveBeenCalledTimes(6);
     expect(h.state.selectionElement).toBeNull();
     expect(h.state.selectionElement).toBeNull();
     expect(h.elements.length).toEqual(1);
     expect(h.elements.length).toEqual(1);
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
@@ -467,8 +467,8 @@ describe("select single element on the scene", () => {
     fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
     fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
     fireEvent.pointerUp(canvas);
     fireEvent.pointerUp(canvas);
 
 
-    expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
-    expect(renderStaticScene).toHaveBeenCalledTimes(7);
+    expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
+    expect(renderStaticScene).toHaveBeenCalledTimes(6);
     expect(h.state.selectionElement).toBeNull();
     expect(h.state.selectionElement).toBeNull();
     expect(h.elements.length).toEqual(1);
     expect(h.elements.length).toEqual(1);
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();

+ 3 - 7
packages/excalidraw/types.ts

@@ -200,7 +200,7 @@ export type InteractiveCanvasAppState = Readonly<
     // SnapLines
     // SnapLines
     snapLines: AppState["snapLines"];
     snapLines: AppState["snapLines"];
     zenModeEnabled: AppState["zenModeEnabled"];
     zenModeEnabled: AppState["zenModeEnabled"];
-    editingElement: AppState["editingElement"];
+    editingTextElement: AppState["editingTextElement"];
   }
   }
 >;
 >;
 
 
@@ -268,13 +268,9 @@ export interface AppState {
   editingFrame: string | null;
   editingFrame: string | null;
   elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
   elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
   /**
   /**
-   * currently set for:
-   * - text elements while in wysiwyg
-   * - newly created linear elements while in line editor (not set for existing
-   *   elements in line editor)
-   * - and new images while being placed on canvas
+   * set when a new text is created or when an existing text is being edited
    */
    */
-  editingElement: NonDeletedExcalidrawElement | null;
+  editingTextElement: NonDeletedExcalidrawElement | null;
   editingLinearElement: LinearElementEditor | null;
   editingLinearElement: LinearElementEditor | null;
   activeTool: {
   activeTool: {
     /**
     /**

+ 1 - 1
packages/utils/__snapshots__/export.test.ts.snap

@@ -29,10 +29,10 @@ exports[`exportToSvg > with default arguments 1`] = `
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
   "defaultSidebarDockedPreference": false,
-  "editingElement": null,
   "editingFrame": null,
   "editingFrame": null,
   "editingGroupId": null,
   "editingGroupId": null,
   "editingLinearElement": null,
   "editingLinearElement": null,
+  "editingTextElement": null,
   "elementsToHighlight": null,
   "elementsToHighlight": null,
   "errorMessage": null,
   "errorMessage": null,
   "exportBackground": true,
   "exportBackground": true,

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно