Forráskód Böngészése

feat: Added penMode for palm rejection (#4657)

Co-authored-by: dwelle <luzar.david@gmail.com>
zsviczian 3 éve
szülő
commit
4486fbc2c6

+ 2 - 0
src/actions/actionCanvas.tsx

@@ -58,6 +58,8 @@ export const actionClearCanvas = register({
         files: {},
         theme: appState.theme,
         elementLocked: appState.elementLocked,
+        penMode: appState.penMode,
+        penDetected: appState.penDetected,
         exportBackground: appState.exportBackground,
         exportEmbedScene: appState.exportEmbedScene,
         gridSize: appState.gridSize,

+ 4 - 0
src/appState.ts

@@ -43,6 +43,8 @@ export const getDefaultAppState = (): Omit<
     editingLinearElement: null,
     elementLocked: false,
     elementType: "selection",
+    penMode: false,
+    penDetected: false,
     errorMessage: null,
     exportBackground: true,
     exportScale: defaultExportScale,
@@ -129,6 +131,8 @@ const APP_STATE_STORAGE_CONF = (<
   editingLinearElement: { browser: false, export: false, server: false },
   elementLocked: { browser: true, export: false, server: false },
   elementType: { browser: true, export: false, server: false },
+  penMode: { browser: false, export: false, server: false },
+  penDetected: { browser: false, export: false, server: false },
   errorMessage: { browser: false, export: false, server: false },
   exportBackground: { browser: true, export: false, server: false },
   exportEmbedScene: { browser: true, export: false, server: false },

+ 35 - 1
src/components/App.tsx

@@ -467,6 +467,7 @@ class App extends React.Component<AppProps, AppState> {
               elements={this.scene.getElements()}
               onCollabButtonClick={onCollabButtonClick}
               onLockToggle={this.toggleLock}
+              onPenModeToggle={this.togglePenMode}
               onInsertElements={(elements) =>
                 this.addElementsFromPasteOrLibrary({
                   elements,
@@ -1501,6 +1502,14 @@ class App extends React.Component<AppProps, AppState> {
     });
   };
 
+  togglePenMode = () => {
+    this.setState((prevState) => {
+      return {
+        penMode: !prevState.penMode,
+      };
+    });
+  };
+
   toggleZenMode = () => {
     this.actionManager.executeAction(actionToggleZenMode);
   };
@@ -2324,7 +2333,10 @@ class App extends React.Component<AppProps, AppState> {
       gesture.lastCenter = center;
 
       const distance = getDistance(Array.from(gesture.pointers.values()));
-      const scaleFactor = distance / gesture.initialDistance;
+      const scaleFactor =
+        this.state.elementType === "freedraw" && this.state.penMode
+          ? 1
+          : distance / gesture.initialDistance;
 
       const nextZoom = scaleFactor
         ? getNormalizedZoom(initialScale * scaleFactor)
@@ -2586,6 +2598,17 @@ class App extends React.Component<AppProps, AppState> {
     this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
     this.maybeCleanupAfterMissingPointerUp(event);
 
+    //fires only once, if pen is detected, penMode is enabled
+    //the user can disable this by toggling the penMode button
+    if (!this.state.penDetected && event.pointerType === "pen") {
+      this.setState((prevState) => {
+        return {
+          penMode: true,
+          penDetected: true,
+        };
+      });
+    }
+
     if (isPanning) {
       return;
     }
@@ -2630,6 +2653,17 @@ class App extends React.Component<AppProps, AppState> {
       return;
     }
 
+    const allowOnPointerDown =
+      !this.state.penMode ||
+      event.pointerType !== "touch" ||
+      this.state.elementType === "selection" ||
+      this.state.elementType === "text" ||
+      this.state.elementType === "image";
+
+    if (!allowOnPointerDown) {
+      return;
+    }
+
     if (this.state.elementType === "text") {
       this.handleTextOnPointerDown(event, pointerDownState);
       return;

+ 11 - 0
src/components/LayerUI.tsx

@@ -36,6 +36,7 @@ import { LibraryMenu } from "./LibraryMenu";
 
 import "./LayerUI.scss";
 import "./Toolbar.scss";
+import { PenModeButton } from "./PenModeButton";
 
 interface LayerUIProps {
   actionManager: ActionManager;
@@ -46,6 +47,7 @@ interface LayerUIProps {
   elements: readonly NonDeletedExcalidrawElement[];
   onCollabButtonClick?: () => void;
   onLockToggle: () => void;
+  onPenModeToggle: () => void;
   onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
   zenModeEnabled: boolean;
   showExitZenModeBtn: boolean;
@@ -76,6 +78,7 @@ const LayerUI = ({
   elements,
   onCollabButtonClick,
   onLockToggle,
+  onPenModeToggle,
   onInsertElements,
   zenModeEnabled,
   showExitZenModeBtn,
@@ -313,6 +316,13 @@ const LayerUI = ({
                       "zen-mode": zenModeEnabled,
                     })}
                   >
+                    <PenModeButton
+                      zenModeEnabled={zenModeEnabled}
+                      checked={appState.penMode}
+                      onChange={onPenModeToggle}
+                      title={t("toolBar.penMode")}
+                      penDetected={appState.penDetected}
+                    />
                     <LockButton
                       zenModeEnabled={zenModeEnabled}
                       checked={appState.elementLocked}
@@ -498,6 +508,7 @@ const LayerUI = ({
         setAppState={setAppState}
         onCollabButtonClick={onCollabButtonClick}
         onLockToggle={onLockToggle}
+        onPenModeToggle={onPenModeToggle}
         canvas={canvas}
         isCollaborating={isCollaborating}
         renderCustomFooter={renderCustomFooter}

+ 10 - 0
src/components/MobileMenu.tsx

@@ -17,6 +17,7 @@ import { LockButton } from "./LockButton";
 import { UserList } from "./UserList";
 import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
 import { LibraryButton } from "./LibraryButton";
+import { PenModeButton } from "./PenModeButton";
 
 type MobileMenuProps = {
   appState: AppState;
@@ -28,6 +29,7 @@ type MobileMenuProps = {
   libraryMenu: JSX.Element | null;
   onCollabButtonClick?: () => void;
   onLockToggle: () => void;
+  onPenModeToggle: () => void;
   canvas: HTMLCanvasElement | null;
   isCollaborating: boolean;
   renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
@@ -50,6 +52,7 @@ export const MobileMenu = ({
   setAppState,
   onCollabButtonClick,
   onLockToggle,
+  onPenModeToggle,
   canvas,
   isCollaborating,
   renderCustomFooter,
@@ -92,6 +95,13 @@ export const MobileMenu = ({
                   setAppState={setAppState}
                   isMobile
                 />
+                <PenModeButton
+                  checked={appState.penMode}
+                  onChange={onPenModeToggle}
+                  title={t("toolBar.penMode")}
+                  isMobile
+                  penDetected={appState.penDetected}
+                />
               </Stack.Row>
               {libraryMenu}
             </Stack.Col>

+ 91 - 0
src/components/PenModeButton.tsx

@@ -0,0 +1,91 @@
+import "./ToolIcon.scss";
+
+import clsx from "clsx";
+import { ToolButtonSize } from "./ToolButton";
+
+type PenModeIconProps = {
+  title?: string;
+  name?: string;
+  checked: boolean;
+  onChange?(): void;
+  zenModeEnabled?: boolean;
+  isMobile?: boolean;
+  penDetected: boolean;
+};
+
+const DEFAULT_SIZE: ToolButtonSize = "medium";
+
+const ICONS = {
+  CHECKED: (
+    <svg
+      width="205"
+      height="205"
+      viewBox="0 0 205 205"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path d="m35 195-25-29.17V50h50v115l-25 30" />
+      <path d="M10 40V10h50v30H10" />
+      <path d="M125 145h70v50h-70" />
+      <path d="M190 145v-30l-10-20h-40l-10 20v30h15v-30l5-5h20l5 5v30h15" />
+    </svg>
+  ),
+  UNCHECKED: (
+    <svg
+      width="205"
+      height="205"
+      viewBox="0 0 205 205"
+      xmlns="http://www.w3.org/2000/svg"
+      className="unlocked-icon rtl-mirror"
+    >
+      <path d="m35 195-25-29.17V50h50v115l-25 30" />
+      <path d="M10 40V10h50v30H10" />
+      <path d="M125 145h70v50h-70" />
+      <path d="M145 145v-30l-10-20H95l-10 20v30h15v-30l5-5h20l5 5v30h15" />
+    </svg>
+  ),
+};
+
+export const PenModeButton = (props: PenModeIconProps) => {
+  if (!props.penDetected) {
+    if (props.isMobile) {
+      return null;
+    }
+    return (
+      <label
+        className={clsx(
+          "ToolIcon ToolIcon__penMode ToolIcon_type_floating",
+          `ToolIcon_size_${DEFAULT_SIZE}`,
+          {
+            "is-mobile": props.isMobile,
+          },
+        )}
+      >
+        <div className="ToolIcon__icon ToolIcon__hidden" />
+      </label>
+    );
+  }
+  return (
+    <label
+      className={clsx(
+        "ToolIcon ToolIcon__penMode ToolIcon_type_floating",
+        `ToolIcon_size_${DEFAULT_SIZE}`,
+        {
+          "is-mobile": props.isMobile,
+        },
+      )}
+      title={`${props.title}`}
+    >
+      <input
+        className="ToolIcon_type_checkbox"
+        type="checkbox"
+        name={props.name}
+        onChange={props.onChange}
+        checked={props.checked}
+        aria-label={props.title}
+      />
+      <div className="ToolIcon__icon">
+        {props.checked ? ICONS.CHECKED : ICONS.UNCHECKED}
+      </div>
+    </label>
+  );
+};

+ 4 - 0
src/components/ToolIcon.scss

@@ -219,6 +219,10 @@
       margin-inline-end: 0;
       top: 60px;
     }
+    .ToolIcon.ToolIcon__penMode {
+      margin-inline-end: 0;
+      top: 140px;
+    }
   }
 
   .unlocked-icon {

+ 4 - 0
src/components/Toolbar.scss

@@ -46,6 +46,10 @@
       }
     }
 
+    .ToolIcon__hidden {
+      box-shadow: none !important;
+    }
+
     .ToolIcon.ToolIcon__lock {
       margin-inline-end: var(--space-factor);
       &.ToolIcon_type_floating {

+ 2 - 1
src/locales/en.json

@@ -187,7 +187,8 @@
     "freedraw": "Draw",
     "text": "Text",
     "library": "Library",
-    "lock": "Keep selected tool active after drawing"
+    "lock": "Keep selected tool active after drawing",
+    "penMode": "Prevent pinch-zoom and accept freedraw input only from pen"
   },
   "headings": {
     "canvasActions": "Canvas actions",

+ 33 - 1
src/tests/__snapshots__/contextmenu.test.tsx.snap

@@ -49,6 +49,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -215,6 +217,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -532,6 +536,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -849,6 +855,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -1015,6 +1023,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -1215,6 +1225,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -1470,6 +1482,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id1": true,
@@ -1805,6 +1819,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -2556,6 +2572,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -2873,6 +2891,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -3190,6 +3210,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id1": true,
@@ -3583,6 +3605,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -3844,6 +3868,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -4182,6 +4208,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -4281,6 +4309,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -4358,6 +4388,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -4551,4 +4583,4 @@ exports[`contextMenu element shows context menu for element: [end of test] numbe
 
 exports[`contextMenu element shows context menu for element: [end of test] number of renders 1`] = `9`;
 
-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`;

+ 105 - 1
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -49,6 +49,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -527,6 +529,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -1011,6 +1015,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -1820,6 +1826,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -2028,6 +2036,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -2503,6 +2513,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -2762,6 +2774,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -2928,6 +2942,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id2": true,
@@ -3386,6 +3402,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -3628,6 +3646,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -3836,6 +3856,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -4086,6 +4108,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id1": true,
@@ -4344,6 +4368,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id2": true,
@@ -4739,6 +4765,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -5042,6 +5070,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -5322,6 +5352,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -5534,6 +5566,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -5700,6 +5734,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -6170,6 +6206,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -6497,6 +6535,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -8615,6 +8655,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -8988,6 +9030,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -9247,6 +9291,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -9469,6 +9515,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -9756,6 +9804,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -9922,6 +9972,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -10088,6 +10140,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -10254,6 +10308,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -10450,6 +10506,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -10646,6 +10704,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -10860,6 +10920,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -11056,6 +11118,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -11222,6 +11286,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -11388,6 +11454,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -11584,6 +11652,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -11750,6 +11820,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -11964,6 +12036,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -12709,6 +12783,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -12968,6 +13044,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -13069,6 +13147,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -13168,6 +13248,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -13337,6 +13419,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -13667,6 +13751,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -13871,6 +13957,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -14728,6 +14816,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -14827,6 +14917,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id0": true,
@@ -15615,6 +15707,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id1": true,
@@ -16033,6 +16127,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {
     "id1": true,
@@ -16313,6 +16409,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -16414,6 +16512,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -16927,6 +17027,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -17026,6 +17128,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,
@@ -17074,4 +17178,4 @@ Object {
 
 exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`;
 
-exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `6`;
+exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `6`;

+ 2 - 0
src/tests/packages/__snapshots__/utils.test.ts.snap

@@ -47,6 +47,8 @@ Object {
     "data": null,
     "shown": false,
   },
+  "penDetected": false,
+  "penMode": false,
   "pendingImageElement": null,
   "previousSelectedElementIds": Object {},
   "resizingElement": null,

+ 2 - 0
src/types.ts

@@ -79,6 +79,8 @@ export type AppState = {
   editingLinearElement: LinearElementEditor | null;
   elementType: typeof SHAPES[number]["value"];
   elementLocked: boolean;
+  penMode: boolean;
+  penDetected: boolean;
   exportBackground: boolean;
   exportEmbedScene: boolean;
   exportWithDarkMode: boolean;