Sfoglia il codice sorgente

Add free draw mode (#1570)

Kostas Bariotis 5 anni fa
parent
commit
9ec43d2626

+ 3 - 3
package-lock.json

@@ -16394,9 +16394,9 @@
       }
     },
     "roughjs": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.0.tgz",
-      "integrity": "sha512-aHEBK0dn50v9HP5hMghQmjpkvPD3He9+pm6UbbcmniFJlIbnvWhw72xFVYR44TorhmwpwtKZj6USniiT0Mq98w==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.1.tgz",
+      "integrity": "sha512-m42+OBaBR7x5UhIKyjBCnWqqkaEkBKLkXvHv4pOWJXPofvMnQY4ZcFEQlqf3coKKyZN2lfWMyx7QXSg2GD7SGA==",
       "requires": {
         "path-data-parser": "^0.1.0",
         "points-on-curve": "^0.2.0",

+ 1 - 1
package.json

@@ -39,7 +39,7 @@
     "react": "16.13.1",
     "react-dom": "16.13.1",
     "react-scripts": "3.4.1",
-    "roughjs": "4.3.0",
+    "roughjs": "4.3.1",
     "socket.io-client": "2.3.0",
     "typescript": "3.8.3"
   },

+ 33 - 14
src/actions/actionFinalize.tsx

@@ -16,31 +16,44 @@ export const actionFinalize = register({
     if (window.document.activeElement instanceof HTMLElement) {
       window.document.activeElement.blur();
     }
-    if (appState.multiElement) {
+
+    const multiPointElement = appState.multiElement
+      ? appState.multiElement
+      : appState.editingElement?.type === "draw"
+      ? appState.editingElement
+      : null;
+
+    if (multiPointElement) {
       // pen and mouse have hover
-      if (appState.lastPointerDownWith !== "touch") {
-        const { points, lastCommittedPoint } = appState.multiElement;
+      if (
+        multiPointElement.type !== "draw" &&
+        appState.lastPointerDownWith !== "touch"
+      ) {
+        const { points, lastCommittedPoint } = multiPointElement;
         if (
           !lastCommittedPoint ||
           points[points.length - 1] !== lastCommittedPoint
         ) {
-          mutateElement(appState.multiElement, {
-            points: appState.multiElement.points.slice(0, -1),
+          mutateElement(multiPointElement, {
+            points: multiPointElement.points.slice(0, -1),
           });
         }
       }
-      if (isInvisiblySmallElement(appState.multiElement)) {
+      if (isInvisiblySmallElement(multiPointElement)) {
         newElements = newElements.slice(0, -1);
       }
 
       // If the multi point line closes the loop,
       // set the last point to first point.
       // This ensures that loop remains closed at different scales.
-      if (appState.multiElement.type === "line") {
-        if (isPathALoop(appState.multiElement.points)) {
-          const linePoints = appState.multiElement.points;
+      if (
+        multiPointElement.type === "line" ||
+        multiPointElement.type === "draw"
+      ) {
+        if (isPathALoop(multiPointElement.points)) {
+          const linePoints = multiPointElement.points;
           const firstPoint = linePoints[0];
-          mutateElement(appState.multiElement, {
+          mutateElement(multiPointElement, {
             points: linePoints.map((point, i) =>
               i === linePoints.length - 1
                 ? ([firstPoint[0], firstPoint[1]] as const)
@@ -51,10 +64,10 @@ export const actionFinalize = register({
       }
 
       if (!appState.elementLocked) {
-        appState.selectedElementIds[appState.multiElement.id] = true;
+        appState.selectedElementIds[multiPointElement.id] = true;
       }
     }
-    if (!appState.elementLocked || !appState.multiElement) {
+    if (!appState.elementLocked || !multiPointElement) {
       resetCursor();
     }
     return {
@@ -62,13 +75,19 @@ export const actionFinalize = register({
       appState: {
         ...appState,
         elementType:
-          appState.elementLocked && appState.multiElement
+          appState.elementLocked && multiPointElement
             ? appState.elementType
             : "selection",
         draggingElement: null,
         multiElement: null,
         editingElement: null,
-        selectedElementIds: {},
+        selectedElementIds:
+          multiPointElement && !appState.elementLocked
+            ? {
+                ...appState.selectedElementIds,
+                [multiPointElement.id]: true,
+              }
+            : appState.selectedElementIds,
       },
       commitToHistory: false,
     };

+ 5 - 5
src/components/Actions.tsx

@@ -94,11 +94,11 @@ export function ShapesSwitcher({
 }) {
   return (
     <>
-      {SHAPES.map(({ value, icon }, index) => {
+      {SHAPES.map(({ value, icon, key }, index) => {
         const label = t(`toolBar.${value}`);
-        const shortcut = `${capitalizeString(value)[0]} ${t(
-          "shortcutsDialog.or",
-        )} ${index + 1}`;
+        const shortcut = `${capitalizeString(key)} ${t("shortcutsDialog.or")} ${
+          index + 1
+        }`;
         return (
           <ToolButton
             key={value}
@@ -109,7 +109,7 @@ export function ShapesSwitcher({
             title={`${capitalizeString(label)} — ${shortcut}`}
             keyBindingLabel={`${index + 1}`}
             aria-label={capitalizeString(label)}
-            aria-keyshortcuts={`${label[0]} ${index + 1}`}
+            aria-keyshortcuts={`${key} ${index + 1}`}
             data-testid={value}
             onChange={() => {
               setAppState({

+ 16 - 4
src/components/App.tsx

@@ -3,6 +3,7 @@ import React from "react";
 import socketIOClient from "socket.io-client";
 import rough from "roughjs/bin/rough";
 import { RoughCanvas } from "roughjs/bin/canvas";
+import { simplify, Point } from "points-on-curve";
 import { FlooredNumber, SocketUpdateData } from "../types";
 
 import {
@@ -1981,6 +1982,7 @@ class App extends React.Component<any, AppState> {
       return;
     } else if (
       this.state.elementType === "arrow" ||
+      this.state.elementType === "draw" ||
       this.state.elementType === "line"
     ) {
       if (this.state.multiElement) {
@@ -2122,7 +2124,7 @@ class App extends React.Component<any, AppState> {
         window.devicePixelRatio,
       );
 
-      // for arrows, don't start dragging until a given threshold
+      // for arrows/lines, don't start dragging until a given threshold
       //  to ensure we don't create a 2-point arrow by mistake when
       //  user clicks mouse in a way that it moves a tiny bit (thus
       //  triggering pointermove)
@@ -2249,9 +2251,15 @@ class App extends React.Component<any, AppState> {
         if (points.length === 1) {
           mutateElement(draggingElement, { points: [...points, [dx, dy]] });
         } else if (points.length > 1) {
-          mutateElement(draggingElement, {
-            points: [...points.slice(0, -1), [dx, dy]],
-          });
+          if (draggingElement.type === "draw") {
+            mutateElement(draggingElement, {
+              points: simplify([...(points as Point[]), [dx, dy]], 0.7),
+            });
+          } else {
+            mutateElement(draggingElement, {
+              points: [...points.slice(0, -1), [dx, dy]],
+            });
+          }
         }
       } else {
         if (getResizeWithSidesSameLengthKey(event)) {
@@ -2330,6 +2338,10 @@ class App extends React.Component<any, AppState> {
       window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
       window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
 
+      if (draggingElement?.type === "draw") {
+        this.actionManager.executeAction(actionFinalize);
+        return;
+      }
       if (isLinearElement(draggingElement)) {
         if (draggingElement!.points.length > 1) {
           history.resumeRecording();

+ 4 - 0
src/components/HintViewer.tsx

@@ -22,6 +22,10 @@ const getHints = ({ appState, elements }: Hint) => {
     return t("hints.linearElementMulti");
   }
 
+  if (elementType === "draw") {
+    return t("hints.freeDraw");
+  }
+
   const selectedElements = getSelectedElements(elements, appState);
   if (
     isResizing &&

+ 2 - 1
src/components/ShortcutsDialog.tsx

@@ -184,7 +184,8 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
               <Shortcut label={t("toolBar.ellipse")} shortcuts={["E", "4"]} />
               <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
               <Shortcut label={t("toolBar.line")} shortcuts={["L", "6"]} />
-              <Shortcut label={t("toolBar.text")} shortcuts={["T", "7"]} />
+              <Shortcut label={t("toolBar.draw")} shortcuts={["X", "7"]} />
+              <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
               <Shortcut
                 label={t("shortcutsDialog.textNewLine")}
                 shortcuts={[

+ 1 - 1
src/data/restore.ts

@@ -43,7 +43,7 @@ export function restore(
           ];
         }
         element.points = points;
-      } else if (element.type === "line") {
+      } else if (element.type === "line" || element.type === "draw") {
         // old spec, pre-arrows
         // old spec, post-arrows
         if (!Array.isArray(element.points) || element.points.length === 0) {

+ 0 - 1
src/element/bounds.ts

@@ -52,7 +52,6 @@ const getMinMaxXYFromCurvePathOps = (
   transformXY?: (x: number, y: number) => [number, number],
 ): [number, number, number, number] => {
   let currentP: Point = [0, 0];
-
   const { minX, minY, maxX, maxY } = ops.reduce(
     (limits, { op, data }) => {
       // There are only four operation types:

+ 1 - 1
src/element/collision.ts

@@ -26,7 +26,7 @@ function isElementDraggableFromInside(
   const dragFromInside =
     element.backgroundColor !== "transparent" ||
     appState.selectedElementIds[element.id];
-  if (element.type === "line") {
+  if (element.type === "line" || element.type === "draw") {
     return dragFromInside && isPathALoop(element.points);
   }
   return dragFromInside;

+ 5 - 1
src/element/handlerRectangles.ts

@@ -187,7 +187,11 @@ export function handlerRectangles(
     pointerType,
   );
 
-  if (element.type === "arrow" || element.type === "line") {
+  if (
+    element.type === "arrow" ||
+    element.type === "line" ||
+    element.type === "draw"
+  ) {
     if (element.points.length === 2) {
       // only check the last point because starting point is always (0,0)
       const [, p1] = element.points;

+ 5 - 1
src/element/sizeHelpers.ts

@@ -21,7 +21,11 @@ export function getPerfectElementSize(
   const absWidth = Math.abs(width);
   const absHeight = Math.abs(height);
 
-  if (elementType === "line" || elementType === "arrow") {
+  if (
+    elementType === "line" ||
+    elementType === "arrow" ||
+    elementType === "draw"
+  ) {
     const lockedAngle =
       Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
       SHIFT_LOCKING_ANGLE;

+ 5 - 1
src/element/typeChecks.ts

@@ -14,7 +14,10 @@ export function isLinearElement(
   element?: ExcalidrawElement | null,
 ): element is ExcalidrawLinearElement {
   return (
-    element != null && (element.type === "arrow" || element.type === "line")
+    element != null &&
+    (element.type === "arrow" ||
+      element.type === "line" ||
+      element.type === "draw")
   );
 }
 
@@ -25,6 +28,7 @@ export function isExcalidrawElement(element: any): boolean {
     element?.type === "rectangle" ||
     element?.type === "ellipse" ||
     element?.type === "arrow" ||
+    element?.type === "draw" ||
     element?.type === "line"
   );
 }

+ 1 - 1
src/element/types.ts

@@ -50,7 +50,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
 
 export type ExcalidrawLinearElement = _ExcalidrawElementBase &
   Readonly<{
-    type: "arrow" | "line";
+    type: "arrow" | "line" | "draw";
     points: Point[];
     lastCommittedPoint?: Point | null;
   }>;

+ 2 - 0
src/locales/en.json

@@ -96,6 +96,7 @@
   },
   "toolBar": {
     "selection": "Selection",
+    "draw": "Free draw",
     "rectangle": "Rectangle",
     "diamond": "Diamond",
     "ellipse": "Ellipse",
@@ -111,6 +112,7 @@
   },
   "hints": {
     "linearElement": "Click to start multiple points, drag for single line",
+    "freeDraw": "Click and drag, release when you're finished",
     "linearElementMulti": "Click on last point or press Escape or Enter to finish",
     "resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center",
     "rotate": "You can constrain angles by holding SHIFT while rotating"

+ 8 - 6
src/renderer/renderElement.ts

@@ -3,7 +3,7 @@ import {
   ExcalidrawTextElement,
   NonDeletedExcalidrawElement,
 } from "../element/types";
-import { isTextElement } from "../element/typeChecks";
+import { isTextElement, isLinearElement } from "../element/typeChecks";
 import {
   getDiamondPoints,
   getArrowPoints,
@@ -35,12 +35,10 @@ function generateElementCanvas(
   const canvas = document.createElement("canvas");
   const context = canvas.getContext("2d")!;
 
-  const isLinear = /\b(arrow|line)\b/.test(element.type);
-
   let canvasOffsetX = 0;
   let canvasOffsetY = 0;
 
-  if (isLinear) {
+  if (isLinearElement(element)) {
     const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
     canvas.width =
       distance(x1, x2) * window.devicePixelRatio * zoom + CANVAS_PADDING * 2;
@@ -90,6 +88,7 @@ function drawElementOnCanvas(
       break;
     }
     case "arrow":
+    case "draw":
     case "line": {
       (getShapeForElement(element) as Drawable[]).forEach((shape) =>
         rc.draw(shape),
@@ -226,6 +225,7 @@ function generateElement(
         );
         break;
       case "line":
+      case "draw":
       case "arrow": {
         const options: Options = {
           stroke: element.strokeColor,
@@ -240,7 +240,7 @@ function generateElement(
 
         // If shape is a line and is a closed shape,
         // fill the shape if a color is set.
-        if (element.type === "line") {
+        if (element.type === "line" || element.type === "draw") {
           if (isPathALoop(element.points)) {
             options.fillStyle = element.fillStyle;
             options.fill =
@@ -343,6 +343,7 @@ export function renderElement(
     case "diamond":
     case "ellipse":
     case "line":
+    case "draw":
     case "arrow":
     case "text": {
       const elementWithCanvas = generateElement(element, generator, sceneState);
@@ -410,6 +411,7 @@ export function renderElementToSvg(
       break;
     }
     case "line":
+    case "draw":
     case "arrow": {
       generateElement(element, generator);
       const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
@@ -427,7 +429,7 @@ export function renderElementToSvg(
           }) rotate(${degree} ${cx} ${cy})`,
         );
         if (
-          element.type === "line" &&
+          (element.type === "line" || element.type === "draw") &&
           isPathALoop(element.points) &&
           element.backgroundColor !== "transparent"
         ) {

+ 2 - 0
src/scene/comparisons.ts

@@ -10,6 +10,7 @@ export const hasBackground = (type: string) =>
   type === "rectangle" ||
   type === "ellipse" ||
   type === "diamond" ||
+  type === "draw" ||
   type === "line";
 
 export const hasStroke = (type: string) =>
@@ -17,6 +18,7 @@ export const hasStroke = (type: string) =>
   type === "ellipse" ||
   type === "diamond" ||
   type === "arrow" ||
+  type === "draw" ||
   type === "line";
 
 export const hasText = (type: string) => type === "text";

+ 22 - 4
src/shapes.tsx

@@ -11,6 +11,7 @@ export const SHAPES = [
       </svg>
     ),
     value: "selection",
+    key: "s",
   },
   {
     icon: (
@@ -20,6 +21,7 @@ export const SHAPES = [
       </svg>
     ),
     value: "rectangle",
+    key: "r",
   },
   {
     icon: (
@@ -29,6 +31,7 @@ export const SHAPES = [
       </svg>
     ),
     value: "diamond",
+    key: "d",
   },
   {
     icon: (
@@ -38,6 +41,7 @@ export const SHAPES = [
       </svg>
     ),
     value: "ellipse",
+    key: "e",
   },
   {
     icon: (
@@ -47,6 +51,7 @@ export const SHAPES = [
       </svg>
     ),
     value: "arrow",
+    key: "a",
   },
   {
     icon: (
@@ -63,6 +68,20 @@ export const SHAPES = [
       </svg>
     ),
     value: "line",
+    key: "l",
+  },
+  {
+    icon: (
+      // fa-pencil
+      <svg viewBox="0 0 512 512">
+        <path
+          fill="currentColor"
+          d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
+        ></path>
+      </svg>
+    ),
+    value: "draw",
+    key: "x",
   },
   {
     icon: (
@@ -72,20 +91,19 @@ export const SHAPES = [
       </svg>
     ),
     value: "text",
+    key: "t",
   },
 ] as const;
 
 export const shapesShortcutKeys = SHAPES.map((shape, index) => [
-  shape.value[0],
+  shape.key,
   (index + 1).toString(),
 ]).flat(1);
 
 export function findShapeByKey(key: string) {
   return (
     SHAPES.find((shape, index) => {
-      return (
-        shape.value[0] === key.toLowerCase() || key === (index + 1).toString()
-      );
+      return shape.key === key.toLowerCase() || key === (index + 1).toString();
     })?.value || "selection"
   );
 }

+ 221 - 4
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -1633,7 +1633,7 @@ Object {
   "currentItemStrokeColor": "#000000",
   "currentItemStrokeWidth": 1,
   "currentItemTextAlign": "left",
-  "cursorButton": "up",
+  "cursorButton": "down",
   "cursorX": 0,
   "cursorY": 0,
   "draggingElement": null,
@@ -1654,7 +1654,9 @@ Object {
   "scrollX": 0,
   "scrollY": 0,
   "scrolledOutside": false,
-  "selectedElementIds": Object {},
+  "selectedElementIds": Object {
+    "id7": true,
+  },
   "selectionElement": null,
   "shouldAddWatermark": false,
   "shouldCacheIgnoreZoom": false,
@@ -1798,6 +1800,39 @@ Object {
 }
 `;
 
+exports[`regression tests draw every type of shape: [end of test] element 5 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "fillStyle": "hachure",
+  "height": 10,
+  "id": "id7",
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "opacity": 100,
+  "points": Array [
+    Array [
+      0,
+      0,
+    ],
+    Array [
+      10,
+      10,
+    ],
+  ],
+  "roughness": 1,
+  "seed": 1051383431,
+  "strokeColor": "#000000",
+  "strokeWidth": 1,
+  "type": "draw",
+  "version": 3,
+  "versionNonce": 1279028647,
+  "width": 10,
+  "x": 30,
+  "y": 10,
+}
+`;
+
 exports[`regression tests draw every type of shape: [end of test] history 1`] = `
 Object {
   "recording": false,
@@ -2248,9 +2283,9 @@ Object {
 }
 `;
 
-exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `5`;
+exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `6`;
 
-exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `34`;
+exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `38`;
 
 exports[`regression tests hotkey 2 selects rectangle tool: [end of test] appState 1`] = `
 Object {
@@ -2901,6 +2936,97 @@ exports[`regression tests hotkey 6 selects line tool: [end of test] number of el
 
 exports[`regression tests hotkey 6 selects line tool: [end of test] number of renders 1`] = `6`;
 
+exports[`regression tests hotkey 7 selects draw tool: [end of test] appState 1`] = `
+Object {
+  "collaborators": Map {},
+  "currentItemBackgroundColor": "transparent",
+  "currentItemFillStyle": "hachure",
+  "currentItemFont": "20px Virgil",
+  "currentItemOpacity": 100,
+  "currentItemRoughness": 1,
+  "currentItemStrokeColor": "#000000",
+  "currentItemStrokeWidth": 1,
+  "currentItemTextAlign": "left",
+  "cursorButton": "down",
+  "cursorX": 0,
+  "cursorY": 0,
+  "draggingElement": null,
+  "editingElement": null,
+  "elementLocked": false,
+  "elementType": "selection",
+  "errorMessage": null,
+  "exportBackground": true,
+  "isCollaborating": false,
+  "isLoading": false,
+  "isResizing": false,
+  "isRotating": false,
+  "lastPointerDownWith": "mouse",
+  "multiElement": null,
+  "name": "Untitled-201933152653",
+  "openMenu": null,
+  "resizingElement": null,
+  "scrollX": 0,
+  "scrollY": 0,
+  "scrolledOutside": false,
+  "selectedElementIds": Object {
+    "id0": true,
+  },
+  "selectionElement": null,
+  "shouldAddWatermark": false,
+  "shouldCacheIgnoreZoom": false,
+  "showShortcutsDialog": false,
+  "username": "",
+  "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
+  "zoom": 1,
+}
+`;
+
+exports[`regression tests hotkey 7 selects draw tool: [end of test] element 0 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "fillStyle": "hachure",
+  "height": 10,
+  "id": "id0",
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "opacity": 100,
+  "points": Array [
+    Array [
+      0,
+      0,
+    ],
+    Array [
+      10,
+      10,
+    ],
+  ],
+  "roughness": 1,
+  "seed": 337897,
+  "strokeColor": "#000000",
+  "strokeWidth": 1,
+  "type": "draw",
+  "version": 3,
+  "versionNonce": 449462985,
+  "width": 10,
+  "x": 10,
+  "y": 10,
+}
+`;
+
+exports[`regression tests hotkey 7 selects draw tool: [end of test] history 1`] = `
+Object {
+  "recording": false,
+  "redoStack": Array [],
+  "stateHistory": Array [],
+}
+`;
+
+exports[`regression tests hotkey 7 selects draw tool: [end of test] number of elements 1`] = `1`;
+
+exports[`regression tests hotkey 7 selects draw tool: [end of test] number of renders 1`] = `6`;
+
 exports[`regression tests hotkey a selects arrow tool: [end of test] appState 1`] = `
 Object {
   "collaborators": Map {},
@@ -3550,6 +3676,97 @@ exports[`regression tests hotkey r selects rectangle tool: [end of test] number
 
 exports[`regression tests hotkey r selects rectangle tool: [end of test] number of renders 1`] = `6`;
 
+exports[`regression tests hotkey x selects draw tool: [end of test] appState 1`] = `
+Object {
+  "collaborators": Map {},
+  "currentItemBackgroundColor": "transparent",
+  "currentItemFillStyle": "hachure",
+  "currentItemFont": "20px Virgil",
+  "currentItemOpacity": 100,
+  "currentItemRoughness": 1,
+  "currentItemStrokeColor": "#000000",
+  "currentItemStrokeWidth": 1,
+  "currentItemTextAlign": "left",
+  "cursorButton": "down",
+  "cursorX": 0,
+  "cursorY": 0,
+  "draggingElement": null,
+  "editingElement": null,
+  "elementLocked": false,
+  "elementType": "selection",
+  "errorMessage": null,
+  "exportBackground": true,
+  "isCollaborating": false,
+  "isLoading": false,
+  "isResizing": false,
+  "isRotating": false,
+  "lastPointerDownWith": "mouse",
+  "multiElement": null,
+  "name": "Untitled-201933152653",
+  "openMenu": null,
+  "resizingElement": null,
+  "scrollX": 0,
+  "scrollY": 0,
+  "scrolledOutside": false,
+  "selectedElementIds": Object {
+    "id0": true,
+  },
+  "selectionElement": null,
+  "shouldAddWatermark": false,
+  "shouldCacheIgnoreZoom": false,
+  "showShortcutsDialog": false,
+  "username": "",
+  "viewBackgroundColor": "#ffffff",
+  "zenModeEnabled": false,
+  "zoom": 1,
+}
+`;
+
+exports[`regression tests hotkey x selects draw tool: [end of test] element 0 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "fillStyle": "hachure",
+  "height": 10,
+  "id": "id0",
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "opacity": 100,
+  "points": Array [
+    Array [
+      0,
+      0,
+    ],
+    Array [
+      10,
+      10,
+    ],
+  ],
+  "roughness": 1,
+  "seed": 337897,
+  "strokeColor": "#000000",
+  "strokeWidth": 1,
+  "type": "draw",
+  "version": 3,
+  "versionNonce": 449462985,
+  "width": 10,
+  "x": 10,
+  "y": 10,
+}
+`;
+
+exports[`regression tests hotkey x selects draw tool: [end of test] history 1`] = `
+Object {
+  "recording": false,
+  "redoStack": Array [],
+  "stateHistory": Array [],
+}
+`;
+
+exports[`regression tests hotkey x selects draw tool: [end of test] number of elements 1`] = `1`;
+
+exports[`regression tests hotkey x selects draw tool: [end of test] number of renders 1`] = `6`;
+
 exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = `
 Object {
   "collaborators": Map {},

+ 1 - 0
src/tests/queries/toolQueries.ts

@@ -7,6 +7,7 @@ const toolMap = {
   ellipse: "ellipse",
   arrow: "arrow",
   line: "line",
+  draw: "draw",
 };
 
 export type ToolName = keyof typeof toolMap;

+ 6 - 0
src/tests/regressionTests.test.tsx

@@ -265,6 +265,11 @@ describe("regression tests", () => {
     pointerMove(30, 50);
     pointerUp();
     hotkeyPress("ENTER");
+
+    clickTool("draw");
+    pointerDown(30, 10);
+    pointerMove(40, 20);
+    pointerUp();
   });
 
   it("click to select a shape", () => {
@@ -290,6 +295,7 @@ describe("regression tests", () => {
     ["4e", "ellipse"],
     ["5a", "arrow"],
     ["6l", "line"],
+    ["7x", "draw"],
   ] as [string, ExcalidrawElement["type"]][]) {
     for (const key of keys) {
       it(`hotkey ${key} selects ${shape} tool`, () => {