From 7d2b6f33742ab3c3028d6ea774f951478384035b Mon Sep 17 00:00:00 2001 From: Alberto Torrigiotti <95320421+atorcode@users.noreply.github.com> Date: Fri, 29 Sep 2023 18:52:53 -0700 Subject: [PATCH 01/18] docs: fix typo on homepage of developer docs (#7047) --- dev-docs/src/components/Homepage/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-docs/src/components/Homepage/index.js b/dev-docs/src/components/Homepage/index.js index b92a294c5..e350e03a2 100644 --- a/dev-docs/src/components/Homepage/index.js +++ b/dev-docs/src/components/Homepage/index.js @@ -15,7 +15,7 @@ const FeatureList = [ Svg: require("@site/static/img/undraw_blank_canvas.svg").default, description: ( <> - Want to build your own app powered by Excalidraw by don't know where to + Want to build your own app powered by Excalidraw but don't know where to start? ), From 84fd13e872147e9a7a7343adb29960e4239bc6fa Mon Sep 17 00:00:00 2001 From: Tanmoy <89120102+tempewda@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:41:02 +0530 Subject: [PATCH 02/18] docs: fix minor grammar and spellings (#7039) --- dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx | 2 +- dev-docs/docs/@excalidraw/excalidraw/integration.mdx | 2 +- dev-docs/docs/introduction/contributing.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx b/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx index 7a1d423fc..9e0f9fcdf 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx @@ -1,6 +1,6 @@ # Customizing Styles -Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors. +Excalidraw uses CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors. Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector: diff --git a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx index 7080c32e9..888f9c5bf 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx @@ -34,7 +34,7 @@ function App() { Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`. -The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon. +The following workflow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon. ```jsx showLineNumbers import { useState, useEffect } from "react"; diff --git a/dev-docs/docs/introduction/contributing.mdx b/dev-docs/docs/introduction/contributing.mdx index 169aa5355..a33cb7a03 100644 --- a/dev-docs/docs/introduction/contributing.mdx +++ b/dev-docs/docs/introduction/contributing.mdx @@ -15,7 +15,7 @@ In case you want to pick up something from the roadmap, comment on that issue an 1. Run `yarn` to install dependencies 1. Create a branch for your PR with `git checkout -b your-branch-name` -> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run: +> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork, run: > > ```bash > git remote add upstream https://github.com/excalidraw/excalidraw.git From 6a821f3b76cd9b3c5fb034e2224cfe44b2d90d9b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 3 Oct 2023 11:07:02 +0200 Subject: [PATCH 03/18] fix: Icon size in manifest (#7073) --- public/manifest.json | 2 +- vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/manifest.json b/public/manifest.json index 64f7e5bac..374d426e6 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -11,7 +11,7 @@ { "src": "apple-touch-icon.png", "type": "image/png", - "sizes": "256x256" + "sizes": "180x180" } ], "start_url": "/", diff --git a/vite.config.ts b/vite.config.ts index 870e21aeb..8d4528886 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -111,7 +111,7 @@ export default defineConfig({ { src: "apple-touch-icon.png", type: "image/png", - sizes: "256x256", + sizes: "180x180", }, ], start_url: "/", From bfd318e76594a8b7f95891d1d029e08305f42d24 Mon Sep 17 00:00:00 2001 From: DanielJGeiger <1852529+DanielJGeiger@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:41:13 -0500 Subject: [PATCH 04/18] docs: Update the `excalidraw-app` source-code link in README.md (#7035) chore: Update the `excalidraw-app` source-code link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a3d1c4d6..a88ab9cc8 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ The Excalidraw editor (npm package) supports: ## Excalidraw.com -The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features: +The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/excalidraw-app) is part of this repository as well, and the app features: - 📡 PWA support (works offline). - 🤼 Real-time collaboration. From 12420592efab734f81738d913683b4b5acaf8d45 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 3 Oct 2023 23:35:47 +0200 Subject: [PATCH 05/18] feat: support menu / dropdown items to have `selected` state (#7078) --- .../excalidraw/api/children-components/main-menu.mdx | 2 ++ src/components/Actions.tsx | 4 ++++ src/components/dropdownMenu/DropdownMenu.scss | 5 +++++ src/components/dropdownMenu/DropdownMenuItem.tsx | 4 +++- src/components/dropdownMenu/DropdownMenuItemCustom.tsx | 6 +++++- src/components/dropdownMenu/DropdownMenuItemLink.tsx | 4 +++- src/components/dropdownMenu/common.ts | 9 +++++++-- src/packages/excalidraw/CHANGELOG.md | 6 ++++++ 8 files changed, 35 insertions(+), 5 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx index 2494df108..77c746b84 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx @@ -38,6 +38,7 @@ To render an item, its recommended to use `MainMenu.Item`. | Prop | Type | Required | Default | Description | | --- | --- | :-: | :-: | --- | | `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. | +| `selected` | `boolean` | No | `false` | Whether item is active | | `children` | `React.ReactNode` | Yes | - | The content of the menu item | | `icon` | `JSX.Element` | No | - | The icon used in the menu item | | `shortcut` | `string` | No | - | The shortcut to be shown for the menu item | @@ -70,6 +71,7 @@ function App() { | Prop | Type | Required | Default | Description | | --- | --- | :-: | :-: | --- | | `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. | +| `selected` | `boolean` | No | `false` | Whether item is active | | `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. | | `children` | `React.ReactNode` | Yes | - | The content of the menu item | | `icon` | `JSX.Element` | No | - | The icon used in the menu item | diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index e9483eac1..47c8df465 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -318,6 +318,7 @@ export const ShapesSwitcher = ({ activeEmbeddable: null, }); }} + selected={activeTool.type === "frame"} /> ) : ( @@ -378,6 +380,7 @@ export const ShapesSwitcher = ({ icon={frameToolIcon} shortcut={KEYS.F.toLocaleUpperCase()} data-testid="toolbar-frame" + selected={activeTool.type === "frame"} > {t("toolBar.frame")} @@ -394,6 +397,7 @@ export const ShapesSwitcher = ({ }} icon={EmbedIcon} data-testid="toolbar-embeddable" + selected={activeTool.type === "embeddable"} > {t("toolBar.embeddable")} diff --git a/src/components/dropdownMenu/DropdownMenu.scss b/src/components/dropdownMenu/DropdownMenu.scss index 6e628736f..1df90fd4a 100644 --- a/src/components/dropdownMenu/DropdownMenu.scss +++ b/src/components/dropdownMenu/DropdownMenu.scss @@ -59,6 +59,11 @@ height: 2.25rem; } + &--selected { + background: var(--color-primary-light); + --icon-fill-color: var(--color-primary-darker); + } + &__text { text-overflow: ellipsis; overflow: hidden; diff --git a/src/components/dropdownMenu/DropdownMenuItem.tsx b/src/components/dropdownMenu/DropdownMenuItem.tsx index 5532dbaae..93108a9f6 100644 --- a/src/components/dropdownMenu/DropdownMenuItem.tsx +++ b/src/components/dropdownMenu/DropdownMenuItem.tsx @@ -11,12 +11,14 @@ const DropdownMenuItem = ({ children, shortcut, className, + selected, ...rest }: { icon?: JSX.Element; onSelect: (event: Event) => void; children: React.ReactNode; shortcut?: string; + selected?: boolean; className?: string; } & Omit, "onSelect">) => { const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect); @@ -26,7 +28,7 @@ const DropdownMenuItem = ({ {...rest} onClick={handleClick} type="button" - className={getDropdownMenuItemClassName(className)} + className={getDropdownMenuItemClassName(className, selected)} title={rest.title ?? rest["aria-label"]} > diff --git a/src/components/dropdownMenu/DropdownMenuItemCustom.tsx b/src/components/dropdownMenu/DropdownMenuItemCustom.tsx index 323f31092..795c5c794 100644 --- a/src/components/dropdownMenu/DropdownMenuItemCustom.tsx +++ b/src/components/dropdownMenu/DropdownMenuItemCustom.tsx @@ -3,15 +3,19 @@ import React from "react"; const DropdownMenuItemCustom = ({ children, className = "", + selected, ...rest }: { children: React.ReactNode; className?: string; + selected?: boolean; } & React.HTMLAttributes) => { return (
{children}
diff --git a/src/components/dropdownMenu/DropdownMenuItemLink.tsx b/src/components/dropdownMenu/DropdownMenuItemLink.tsx index c93749ac2..0dacaebf9 100644 --- a/src/components/dropdownMenu/DropdownMenuItemLink.tsx +++ b/src/components/dropdownMenu/DropdownMenuItemLink.tsx @@ -12,6 +12,7 @@ const DropdownMenuItemLink = ({ children, onSelect, className = "", + selected, ...rest }: { href: string; @@ -19,6 +20,7 @@ const DropdownMenuItemLink = ({ children: React.ReactNode; shortcut?: string; className?: string; + selected?: boolean; onSelect?: (event: Event) => void; } & React.AnchorHTMLAttributes) => { const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect); @@ -29,7 +31,7 @@ const DropdownMenuItemLink = ({ href={href} target="_blank" rel="noreferrer" - className={getDropdownMenuItemClassName(className)} + className={getDropdownMenuItemClassName(className, selected)} title={rest.title ?? rest["aria-label"]} onClick={handleClick} > diff --git a/src/components/dropdownMenu/common.ts b/src/components/dropdownMenu/common.ts index 9b5e7bfe9..c59584584 100644 --- a/src/components/dropdownMenu/common.ts +++ b/src/components/dropdownMenu/common.ts @@ -6,8 +6,13 @@ export const DropdownMenuContentPropsContext = React.createContext<{ onSelect?: (event: Event) => void; }>({}); -export const getDropdownMenuItemClassName = (className = "") => { - return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim(); +export const getDropdownMenuItemClassName = ( + className = "", + selected = false, +) => { + return `dropdown-menu-item dropdown-menu-item-base ${className} ${ + selected ? "dropdown-menu-item--selected" : "" + }`.trim(); }; export const useHandleDropdownMenuItemClick = ( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index c3e8be9a0..b852757d3 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,6 +11,12 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> +## Unreleased + +### Features + +- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [7078](https://github.com/excalidraw/excalidraw/pull/7078) + ## 0.16.1 (2023-09-21) ## Excalidraw Library From 1f4f5e11aec9d8e1bceb3ece40c6c9d69a3f553e Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 4 Oct 2023 00:16:54 +0200 Subject: [PATCH 06/18] refactor: DRY out and simplify setting active tool from toolbar (#7079) --- src/components/Actions.tsx | 80 +++---------------- src/components/App.tsx | 44 +++++----- src/components/LayerUI.tsx | 2 +- src/components/MobileMenu.tsx | 2 +- .../__snapshots__/contextmenu.test.tsx.snap | 28 +++---- .../regressionTests.test.tsx.snap | 75 +++++++++-------- src/tests/dragCreate.test.tsx | 20 ++--- src/tests/move.test.tsx | 6 +- src/tests/multiPointCreate.test.tsx | 10 +-- src/tests/selection.test.tsx | 10 +-- src/types.ts | 4 +- 11 files changed, 116 insertions(+), 165 deletions(-) diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 47c8df465..a176ee5ab 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -14,13 +14,8 @@ import { hasText, } from "../scene"; import { SHAPES } from "../shapes"; -import { UIAppState, Zoom } from "../types"; -import { - capitalizeString, - isTransparent, - updateActiveTool, - setCursorForShape, -} from "../utils"; +import { AppClassProperties, UIAppState, Zoom } from "../types"; +import { capitalizeString, isTransparent } from "../utils"; import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; import { hasStrokeColor } from "../scene/comparisons"; @@ -215,15 +210,15 @@ export const SelectedShapeActions = ({ export const ShapesSwitcher = ({ interactiveCanvas, activeTool, - setAppState, onImageAction, appState, + app, }: { interactiveCanvas: HTMLCanvasElement | null; activeTool: UIAppState["activeTool"]; - setAppState: React.Component["setState"]; onImageAction: (data: { pointerType: PointerType | null }) => void; appState: UIAppState; + app: AppClassProperties; }) => { const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false); const device = useDevice(); @@ -251,29 +246,14 @@ export const ShapesSwitcher = ({ data-testid={`toolbar-${value}`} onPointerDown={({ pointerType }) => { if (!appState.penDetected && pointerType === "pen") { - setAppState({ - penDetected: true, - penMode: true, - }); + app.togglePenMode(true); } }} onChange={({ pointerType }) => { if (appState.activeTool.type !== value) { trackEvent("toolbar", value, "ui"); } - const nextActiveTool = updateActiveTool(appState, { - type: value, - }); - setAppState({ - activeTool: nextActiveTool, - activeEmbeddable: null, - multiElement: null, - selectedElementIds: {}, - }); - setCursorForShape(interactiveCanvas, { - ...appState, - activeTool: nextActiveTool, - }); + app.setActiveTool({ type: value }); if (value === "image") { onImageAction({ pointerType }); } @@ -300,23 +280,12 @@ export const ShapesSwitcher = ({ data-testid={`toolbar-frame`} onPointerDown={({ pointerType }) => { if (!appState.penDetected && pointerType === "pen") { - setAppState({ - penDetected: true, - penMode: true, - }); + app.togglePenMode(true); } }} onChange={({ pointerType }) => { trackEvent("toolbar", "frame", "ui"); - const nextActiveTool = updateActiveTool(appState, { - type: "frame", - }); - setAppState({ - activeTool: nextActiveTool, - multiElement: null, - selectedElementIds: {}, - activeEmbeddable: null, - }); + app.setActiveTool({ type: "frame" }); }} selected={activeTool.type === "frame"} /> @@ -331,23 +300,12 @@ export const ShapesSwitcher = ({ data-testid={`toolbar-embeddable`} onPointerDown={({ pointerType }) => { if (!appState.penDetected && pointerType === "pen") { - setAppState({ - penDetected: true, - penMode: true, - }); + app.togglePenMode(true); } }} onChange={({ pointerType }) => { trackEvent("toolbar", "embeddable", "ui"); - const nextActiveTool = updateActiveTool(appState, { - type: "embeddable", - }); - setAppState({ - activeTool: nextActiveTool, - multiElement: null, - selectedElementIds: {}, - activeEmbeddable: null, - }); + app.setActiveTool({ type: "embeddable" }); }} selected={activeTool.type === "embeddable"} /> @@ -368,14 +326,7 @@ export const ShapesSwitcher = ({ > { - const nextActiveTool = updateActiveTool(appState, { - type: "frame", - }); - setAppState({ - activeTool: nextActiveTool, - multiElement: null, - selectedElementIds: {}, - }); + app.setActiveTool({ type: "frame" }); }} icon={frameToolIcon} shortcut={KEYS.F.toLocaleUpperCase()} @@ -386,14 +337,7 @@ export const ShapesSwitcher = ({ { - const nextActiveTool = updateActiveTool(appState, { - type: "embeddable", - }); - setAppState({ - activeTool: nextActiveTool, - multiElement: null, - selectedElementIds: {}, - }); + app.setActiveTool({ type: "embeddable" }); }} icon={EmbedIcon} data-testid="toolbar-embeddable" diff --git a/src/components/App.tsx b/src/components/App.tsx index 3fb4e5e73..b91e0e4b5 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2552,10 +2552,11 @@ class App extends React.Component { }); }; - togglePenMode = () => { + togglePenMode = (force?: boolean) => { this.setState((prevState) => { return { - penMode: !prevState.penMode, + penMode: force ?? !prevState.penMode, + penDetected: true, }; }); }; @@ -3108,7 +3109,7 @@ class App extends React.Component { } }); - private setActiveTool = ( + setActiveTool = ( tool: | { type: @@ -3135,23 +3136,30 @@ class App extends React.Component { if (nextActiveTool.type === "image") { this.onImageAction(); } - if (nextActiveTool.type !== "selection") { - this.setState((prevState) => ({ - activeTool: nextActiveTool, - selectedElementIds: makeNextSelectedElementIds({}, this.state), - selectedGroupIds: {}, - editingGroupId: null, - snapLines: [], - originSnapOffset: null, - })); - } else { - this.setState({ - activeTool: nextActiveTool, - snapLines: [], + + this.setState((prevState) => { + const commonResets = { + snapLines: prevState.snapLines.length ? [] : prevState.snapLines, originSnapOffset: null, activeEmbeddable: null, - }); - } + } as const; + if (nextActiveTool.type !== "selection") { + return { + ...prevState, + activeTool: nextActiveTool, + selectedElementIds: makeNextSelectedElementIds({}, prevState), + selectedGroupIds: makeNextSelectedElementIds({}, prevState), + editingGroupId: null, + multiElement: null, + ...commonResets, + }; + } + return { + ...prevState, + activeTool: nextActiveTool, + ...commonResets, + }; + }); }; private setCursor = (cursor: string) => { diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 3bc8436cc..d9d3d8ce4 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -279,7 +279,7 @@ const LayerUI = ({ appState={appState} interactiveCanvas={interactiveCanvas} activeTool={appState.activeTool} - setAppState={setAppState} + app={app} onImageAction={({ pointerType }) => { onImageAction({ insertOnCanvasDirectly: pointerType !== "mouse", diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index f08b99969..b7f0ba333 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -87,7 +87,7 @@ export const MobileMenu = ({ appState={appState} interactiveCanvas={interactiveCanvas} activeTool={appState.activeTool} - setAppState={setAppState} + app={app} onImageAction={({ pointerType }) => { onImageAction({ insertOnCanvasDirectly: pointerType !== "mouse", diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 5f9492225..0cdd20b25 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -675,7 +675,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e 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`] = `7`; +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 'Bring forward' in context menu brings element forward > [end of test] appState 1`] = ` { @@ -1051,7 +1051,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings 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`] = `13`; +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 to front' in context menu brings element to front > [end of test] appState 1`] = ` { @@ -1427,7 +1427,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings 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`] = `13`; +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 'Copy styles' in context menu copies styles > [end of test] appState 1`] = ` { @@ -1629,7 +1629,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st 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`] = `7`; +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `6`; exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] appState 1`] = ` { @@ -1868,7 +1868,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen 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`] = `8`; +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `7`; exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] appState 1`] = ` { @@ -2172,7 +2172,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates 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`] = `8`; +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `7`; exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] appState 1`] = ` { @@ -2564,7 +2564,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group 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`] = `13`; +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `12`; exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] appState 1`] = ` { @@ -3446,7 +3446,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s 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`] = `20`; +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `19`; exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] appState 1`] = ` { @@ -3822,7 +3822,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e 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`] = `12`; +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 to back' in context menu sends element to back > [end of test] appState 1`] = ` { @@ -4198,7 +4198,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el 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`] = `12`; +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 'Ungroup selection' in context menu ungroups selected group > [end of test] appState 1`] = ` { @@ -4657,7 +4657,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung 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`] = `14`; +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `13`; exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] appState 1`] = ` { @@ -5240,7 +5240,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi 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`] = `13`; +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 'Ungroup selection' in context menu for group inside selected elements > [end of test] appState 1`] = ` { @@ -5908,7 +5908,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro 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`] = `14`; +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 context menu for canvas > [end of test] appState 1`] = ` { @@ -7104,6 +7104,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 renders 1`] = `7`; +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 2`] = `6`; diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 32dd17545..c990a6e03 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -454,7 +454,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of elements 1`] = `0`; -exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of renders 1`] = `22`; +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of renders 1`] = `21`; exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] appState 1`] = ` { @@ -912,7 +912,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of elements 1`] = `0`; -exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of renders 1`] = `20`; +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of renders 1`] = `19`; exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] appState 1`] = ` { @@ -1743,7 +1743,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of elements 1`] = `0`; -exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `30`; +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `29`; exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] appState 1`] = ` { @@ -1956,7 +1956,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `10`; +exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `9`; exports[`regression tests > adjusts z order when grouping > [end of test] appState 1`] = ` { @@ -2410,7 +2410,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor exports[`regression tests > adjusts z order when grouping > [end of test] number of elements 1`] = `0`; -exports[`regression tests > adjusts z order when grouping > [end of test] number of renders 1`] = `19`; +exports[`regression tests > adjusts z order when grouping > [end of test] number of renders 1`] = `18`; exports[`regression tests > alt-drag duplicates an element > [end of test] appState 1`] = ` { @@ -2652,7 +2652,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo exports[`regression tests > alt-drag duplicates an element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > alt-drag duplicates an element > [end of test] number of renders 1`] = `10`; +exports[`regression tests > alt-drag duplicates an element > [end of test] number of renders 1`] = `9`; exports[`regression tests > arrow keys > [end of test] appState 1`] = ` { @@ -2820,7 +2820,7 @@ exports[`regression tests > arrow keys > [end of test] history 1`] = ` exports[`regression tests > arrow keys > [end of test] number of elements 1`] = `0`; -exports[`regression tests > arrow keys > [end of test] number of renders 1`] = `14`; +exports[`regression tests > arrow keys > [end of test] number of renders 1`] = `13`; exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] appState 1`] = ` { @@ -3264,7 +3264,7 @@ exports[`regression tests > can drag element that covers another element, while exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of renders 1`] = `19`; +exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of renders 1`] = `18`; exports[`regression tests > change the properties of a shape > [end of test] appState 1`] = ` { @@ -3561,7 +3561,7 @@ exports[`regression tests > change the properties of a shape > [end of test] his exports[`regression tests > change the properties of a shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > change the properties of a shape > [end of test] number of renders 1`] = `11`; +exports[`regression tests > change the properties of a shape > [end of test] number of renders 1`] = `10`; exports[`regression tests > click on an element and drag it > [dragged] appState 1`] = ` { @@ -3806,7 +3806,7 @@ exports[`regression tests > click on an element and drag it > [dragged] history exports[`regression tests > click on an element and drag it > [dragged] number of elements 1`] = `1`; -exports[`regression tests > click on an element and drag it > [dragged] number of renders 1`] = `10`; +exports[`regression tests > click on an element and drag it > [dragged] number of renders 1`] = `9`; exports[`regression tests > click on an element and drag it > [end of test] appState 1`] = ` { @@ -4062,7 +4062,7 @@ exports[`regression tests > click on an element and drag it > [end of test] hist exports[`regression tests > click on an element and drag it > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click on an element and drag it > [end of test] number of renders 1`] = `12`; +exports[`regression tests > click on an element and drag it > [end of test] number of renders 1`] = `11`; exports[`regression tests > click to select a shape > [end of test] appState 1`] = ` { @@ -4304,7 +4304,7 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] exports[`regression tests > click to select a shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click to select a shape > [end of test] number of renders 1`] = `13`; +exports[`regression tests > click to select a shape > [end of test] number of renders 1`] = `12`; exports[`regression tests > click-drag to select a group > [end of test] appState 1`] = ` { @@ -4648,7 +4648,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history exports[`regression tests > click-drag to select a group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `18`; +exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `17`; exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] appState 1`] = ` { @@ -5123,7 +5123,7 @@ exports[`regression tests > deleting last but one element in editing group shoul exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of renders 1`] = `20`; +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of renders 1`] = `19`; exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` { @@ -5420,7 +5420,7 @@ exports[`regression tests > deselects group of selected elements on pointer down exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `14`; +exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `13`; exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] appState 1`] = ` { @@ -5689,7 +5689,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of renders 1`] = `14`; +exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of renders 1`] = `13`; exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` { @@ -5913,7 +5913,7 @@ exports[`regression tests > deselects selected element on pointer down when poin exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `9`; +exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `8`; exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] appState 1`] = ` { @@ -6081,7 +6081,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of renders 1`] = `9`; +exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of renders 1`] = `8`; exports[`regression tests > double click to edit a group > [end of test] appState 1`] = ` { @@ -6533,7 +6533,7 @@ exports[`regression tests > double click to edit a group > [end of test] history exports[`regression tests > double click to edit a group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > double click to edit a group > [end of test] number of renders 1`] = `19`; +exports[`regression tests > double click to edit a group > [end of test] number of renders 1`] = `18`; exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] appState 1`] = ` { @@ -6850,7 +6850,7 @@ exports[`regression tests > drags selected elements from point inside common bou exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of elements 1`] = `0`; -exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of renders 1`] = `15`; +exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of renders 1`] = `14`; exports[`regression tests > draw every type of shape > [end of test] appState 1`] = ` { @@ -8917,7 +8917,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `44`; +exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `43`; exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = ` { @@ -9261,7 +9261,7 @@ exports[`regression tests > given a group of selected elements with an element t exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of renders 1`] = `18`; +exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of renders 1`] = `17`; exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] appState 1`] = ` { @@ -9504,7 +9504,7 @@ exports[`regression tests > given a selected element A and a not selected elemen exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of renders 1`] = `15`; +exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of renders 1`] = `14`; exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] appState 1`] = ` { @@ -12842,7 +12842,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor exports[`regression tests > make a group and duplicate it > [end of test] number of elements 1`] = `0`; -exports[`regression tests > make a group and duplicate it > [end of test] number of renders 1`] = `21`; +exports[`regression tests > make a group and duplicate it > [end of test] number of renders 1`] = `20`; exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] appState 1`] = ` { @@ -13084,7 +13084,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of elements 1`] = `0`; -exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of renders 1`] = `17`; +exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of renders 1`] = `16`; exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` { @@ -13375,7 +13375,7 @@ exports[`regression tests > shift click on selected element should deselect it o exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of renders 1`] = `9`; +exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of renders 1`] = `8`; exports[`regression tests > shift-click to multiselect, then drag > [end of test] appState 1`] = ` { @@ -13692,7 +13692,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of elements 1`] = `0`; -exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of renders 1`] = `16`; +exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of renders 1`] = `15`; exports[`regression tests > should group elements and ungroup them > [end of test] appState 1`] = ` { @@ -14253,7 +14253,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes exports[`regression tests > should group elements and ungroup them > [end of test] number of elements 1`] = `0`; -exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `22`; +exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `21`; exports[`regression tests > should show fill icons when element has non transparent background > [end of test] appState 1`] = ` { @@ -14464,7 +14464,7 @@ exports[`regression tests > should show fill icons when element has non transpar exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of elements 1`] = `0`; -exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `10`; +exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `9`; exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = ` { @@ -15320,7 +15320,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of elements 1`] = `0`; -exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `30`; +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `29`; exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] appState 1`] = ` { @@ -16233,7 +16233,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = exports[`regression tests > supports nested groups > [end of test] number of elements 1`] = `0`; -exports[`regression tests > supports nested groups > [end of test] number of renders 1`] = `27`; +exports[`regression tests > supports nested groups > [end of test] number of renders 1`] = `26`; exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] appState 1`] = ` { @@ -16633,7 +16633,7 @@ exports[`regression tests > switches from group of selected elements to another exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of elements 1`] = `0`; -exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of renders 1`] = `18`; +exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of renders 1`] = `17`; exports[`regression tests > switches selected element on pointer down > [end of test] appState 1`] = ` { @@ -16931,7 +16931,7 @@ exports[`regression tests > switches selected element on pointer down > [end of exports[`regression tests > switches selected element on pointer down > [end of test] number of elements 1`] = `0`; -exports[`regression tests > switches selected element on pointer down > [end of test] number of renders 1`] = `13`; +exports[`regression tests > switches selected element on pointer down > [end of test] number of renders 1`] = `12`; exports[`regression tests > two-finger scroll works > [end of test] appState 1`] = ` { @@ -17537,7 +17537,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `24`; +exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `23`; exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = ` { @@ -17601,10 +17601,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": null, "pasteDialog": { "data": null, "shown": false, @@ -17663,7 +17660,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of elements 1`] = `0`; -exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of renders 1`] = `6`; +exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of renders 1`] = `5`; exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` { diff --git a/src/tests/dragCreate.test.tsx b/src/tests/dragCreate.test.tsx index 6e6c1dbe4..f1808f873 100644 --- a/src/tests/dragCreate.test.tsx +++ b/src/tests/dragCreate.test.tsx @@ -47,7 +47,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -79,7 +79,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); @@ -112,7 +112,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -144,7 +144,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -180,7 +180,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -221,7 +221,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -241,7 +241,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -261,7 +261,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -288,7 +288,7 @@ describe("Test dragCreate", () => { }); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -315,7 +315,7 @@ describe("Test dragCreate", () => { }); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); diff --git a/src/tests/move.test.tsx b/src/tests/move.test.tsx index 6e2d60083..eab21bfba 100644 --- a/src/tests/move.test.tsx +++ b/src/tests/move.test.tsx @@ -43,7 +43,7 @@ describe("move element", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -85,7 +85,7 @@ describe("move element", () => { new Pointer("mouse").clickOn(rectB); expect(renderInteractiveScene).toHaveBeenCalledTimes(24); - expect(renderStaticScene).toHaveBeenCalledTimes(20); + expect(renderStaticScene).toHaveBeenCalledTimes(19); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(3); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); @@ -131,7 +131,7 @@ describe("duplicate element on move when ALT is clicked", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); diff --git a/src/tests/multiPointCreate.test.tsx b/src/tests/multiPointCreate.test.tsx index bf23955cc..53da14b12 100644 --- a/src/tests/multiPointCreate.test.tsx +++ b/src/tests/multiPointCreate.test.tsx @@ -48,7 +48,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.elements.length).toEqual(0); }); @@ -63,7 +63,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.elements.length).toEqual(0); }); @@ -78,7 +78,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.elements.length).toEqual(0); }); }); @@ -111,7 +111,7 @@ describe("multi point mode in linear elements", () => { }); expect(renderInteractiveScene).toHaveBeenCalledTimes(11); - expect(renderStaticScene).toHaveBeenCalledTimes(10); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; @@ -154,7 +154,7 @@ describe("multi point mode in linear elements", () => { key: KEYS.ENTER, }); expect(renderInteractiveScene).toHaveBeenCalledTimes(11); - expect(renderStaticScene).toHaveBeenCalledTimes(10); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index f6c83fc7a..4f1c27015 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -310,7 +310,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -342,7 +342,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -374,7 +374,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -419,7 +419,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -463,7 +463,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); diff --git a/src/types.ts b/src/types.ts index af8212355..991de9f19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -292,7 +292,7 @@ export type AppState = { showHyperlinkPopup: false | "info" | "editor"; selectedLinearElement: LinearElementEditor | null; - snapLines: SnapLine[]; + snapLines: readonly SnapLine[]; originSnapOffset: { x: number; y: number; @@ -539,6 +539,8 @@ export type AppClassProperties = { onInsertElements: App["onInsertElements"]; onExportImage: App["onExportImage"]; lastViewportPosition: App["lastViewportPosition"]; + togglePenMode: App["togglePenMode"]; + setActiveTool: App["setActiveTool"]; }; export type PointerDownState = Readonly<{ From 8b838049dfbf6f0aef3e61e51b52e648aa1eba69 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 4 Oct 2023 16:09:59 +0200 Subject: [PATCH 07/18] fix: remove invisible elements safely (#7083) --- src/actions/actionFinalize.tsx | 4 +++- src/components/App.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index c25e2ef4d..4d422994c 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -90,7 +90,9 @@ export const actionFinalize = register({ } } if (isInvisiblySmallElement(multiPointElement)) { - newElements = newElements.slice(0, -1); + newElements = newElements.filter( + (el) => el.id !== multiPointElement.id, + ); } // If the multi point line closes the loop, diff --git a/src/components/App.tsx b/src/components/App.tsx index b91e0e4b5..2e7c468eb 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -6549,7 +6549,9 @@ class App extends React.Component { ) { // remove invisible element which was added in onPointerDown this.scene.replaceAllElements( - this.scene.getElementsIncludingDeleted().slice(0, -1), + this.scene + .getElementsIncludingDeleted() + .filter((el) => el.id !== draggingElement.id), ); this.setState({ draggingElement: null, From fa33aa08abcc929aa1ba52ad01728ea0d1f0a568 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 4 Oct 2023 16:18:22 +0200 Subject: [PATCH 08/18] refactor: refactor event globals to differentiate from `lastPointerUp` (#7084) --- src/components/App.tsx | 62 +++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 2e7c468eb..2372f0a22 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -491,8 +491,9 @@ class App extends React.Component { private iFrameRefs = new Map(); hitLinkElement?: NonDeletedExcalidrawElement; - lastPointerDown: React.PointerEvent | null = null; - lastPointerUp: React.PointerEvent | PointerEvent | null = null; + lastPointerDownEvent: React.PointerEvent | null = null; + lastPointerUpEvent: React.PointerEvent | PointerEvent | null = + null; lastViewportPosition = { x: 0, y: 0 }; constructor(props: AppProps) { @@ -3736,10 +3737,10 @@ class App extends React.Component { isTouchScreen: boolean, ) => { const draggedDistance = distance2d( - this.lastPointerDown!.clientX, - this.lastPointerDown!.clientY, - this.lastPointerUp!.clientX, - this.lastPointerUp!.clientY, + this.lastPointerDownEvent!.clientX, + this.lastPointerDownEvent!.clientY, + this.lastPointerUpEvent!.clientX, + this.lastPointerUpEvent!.clientY, ); if ( !this.hitLinkElement || @@ -3750,7 +3751,7 @@ class App extends React.Component { return; } const lastPointerDownCoords = viewportCoordsToSceneCoords( - this.lastPointerDown!, + this.lastPointerDownEvent!, this.state, ); const lastPointerDownHittingLinkIcon = isPointHittingLink( @@ -3760,7 +3761,7 @@ class App extends React.Component { this.device.isMobile, ); const lastPointerUpCoords = viewportCoordsToSceneCoords( - this.lastPointerUp!, + this.lastPointerUpEvent!, this.state, ); const lastPointerUpHittingLinkIcon = isPointHittingLink( @@ -4465,7 +4466,8 @@ class App extends React.Component { return; } - this.lastPointerDown = event; + this.lastPointerDownEvent = event; + this.setState({ lastPointerDownWith: event.pointerType, cursorButton: "down", @@ -4605,14 +4607,14 @@ class App extends React.Component { event: React.PointerEvent, ) => { this.removePointer(event); - this.lastPointerUp = event; + this.lastPointerUpEvent = event; const scenePointer = viewportCoordsToSceneCoords( { clientX: event.clientX, clientY: event.clientY }, this.state, ); const clicklength = - event.timeStamp - (this.lastPointerDown?.timeStamp ?? 0); + event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0); if (this.device.isMobile && clicklength < 300) { const hitElement = this.getElementAtPosition( scenePointer.x, @@ -5366,7 +5368,9 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( sceneX, sceneY, - this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] + ? null + : this.state.gridSize, ); const embedLink = getEmbedLink(link); @@ -5416,7 +5420,9 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( sceneX, sceneY, - this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] + ? null + : this.state.gridSize, ); const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ @@ -5593,7 +5599,9 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, pointerDownState.origin.y, - this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] + ? null + : this.state.gridSize, ); const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ @@ -5651,7 +5659,9 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, pointerDownState.origin.y, - this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] + ? null + : this.state.gridSize, ); const frame = newFrameElement({ @@ -6773,17 +6783,17 @@ class App extends React.Component { } if (isEraserActive(this.state)) { const draggedDistance = distance2d( - this.lastPointerDown!.clientX, - this.lastPointerDown!.clientY, - this.lastPointerUp!.clientX, - this.lastPointerUp!.clientY, + this.lastPointerDownEvent!.clientX, + this.lastPointerDownEvent!.clientY, + this.lastPointerUpEvent!.clientX, + this.lastPointerUpEvent!.clientY, ); if (draggedDistance === 0) { const scenePointer = viewportCoordsToSceneCoords( { - clientX: this.lastPointerUp!.clientX, - clientY: this.lastPointerUp!.clientY, + clientX: this.lastPointerUpEvent!.clientX, + clientY: this.lastPointerUpEvent!.clientY, }, this.state, ); @@ -7039,14 +7049,16 @@ class App extends React.Component { if ( hitElement && - this.lastPointerUp && - this.lastPointerDown && - this.lastPointerUp.timeStamp - this.lastPointerDown.timeStamp < 300 && + this.lastPointerUpEvent && + this.lastPointerDownEvent && + this.lastPointerUpEvent.timeStamp - + this.lastPointerDownEvent.timeStamp < + 300 && gesture.pointers.size <= 1 && isEmbeddableElement(hitElement) && this.isEmbeddableCenter( hitElement, - this.lastPointerUp, + this.lastPointerUpEvent, pointerDownState.origin.x, pointerDownState.origin.y, ) From e6f74350ac7d561646475642597b8a27e103bb98 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 4 Oct 2023 23:39:00 +0200 Subject: [PATCH 09/18] refactor: DRY out tool typing (#7086) --- src/components/App.tsx | 10 +++------ src/types.ts | 46 ++++++++++++++++++------------------------ src/utils.ts | 12 +++-------- 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 2372f0a22..8a9059557 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -211,7 +211,7 @@ import { import Scene from "../scene/Scene"; import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types"; import { getStateForZoom } from "../scene/zoom"; -import { findShapeByKey, SHAPES } from "../shapes"; +import { findShapeByKey } from "../shapes"; import { AppClassProperties, AppProps, @@ -230,6 +230,7 @@ import { SidebarName, SidebarTabName, KeyboardModifiersObject, + ToolType, } from "../types"; import { debounce, @@ -3113,12 +3114,7 @@ class App extends React.Component { setActiveTool = ( tool: | { - type: - | typeof SHAPES[number]["value"] - | "eraser" - | "hand" - | "frame" - | "embeddable"; + type: ToolType; } | { type: "custom"; customType: string }, ) => { diff --git a/src/types.ts b/src/types.ts index 991de9f19..c3b0252e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,7 +18,6 @@ import { ExcalidrawFrameElement, ExcalidrawEmbeddableElement, } from "./element/types"; -import { SHAPES } from "./shapes"; import { Point as RoughPoint } from "roughjs/bin/geometry"; import { LinearElementEditor } from "./element/linearElementEditor"; import { SuggestedBinding } from "./element/binding"; @@ -86,21 +85,30 @@ export type BinaryFileMetadata = Omit; export type BinaryFiles = Record; -export type LastActiveTool = +export type ToolType = + | "selection" + | "rectangle" + | "diamond" + | "ellipse" + | "arrow" + | "line" + | "freedraw" + | "text" + | "image" + | "eraser" + | "hand" + | "frame" + | "embeddable"; + +export type ActiveTool = | { - type: - | typeof SHAPES[number]["value"] - | "eraser" - | "hand" - | "frame" - | "embeddable"; + type: ToolType; customType: null; } | { type: "custom"; customType: string; - } - | null; + }; export type SidebarName = string; export type SidebarTabName = string; @@ -195,23 +203,9 @@ export type AppState = { * indicates a previous tool we should revert back to if we deselect the * currently active tool. At the moment applies to `eraser` and `hand` tool. */ - lastActiveTool: LastActiveTool; + lastActiveTool: ActiveTool | null; locked: boolean; - } & ( - | { - type: - | typeof SHAPES[number]["value"] - | "eraser" - | "hand" - | "frame" - | "embeddable"; - customType: null; - } - | { - type: "custom"; - customType: string; - } - ); + } & ActiveTool; penMode: boolean; penDetected: boolean; exportBackground: boolean; diff --git a/src/utils.ts b/src/utils.ts index 8b142744a..5acd1b664 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,9 +15,8 @@ import { FontString, NonDeletedExcalidrawElement, } from "./element/types"; -import { AppState, DataURL, LastActiveTool, Zoom } from "./types"; +import { ActiveTool, AppState, DataURL, ToolType, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; -import { SHAPES } from "./shapes"; import { isEraserActive, isHandToolActive } from "./appState"; import { ResolutionType } from "./utility-types"; import React from "react"; @@ -371,15 +370,10 @@ export const updateActiveTool = ( appState: Pick, data: ( | { - type: - | typeof SHAPES[number]["value"] - | "eraser" - | "hand" - | "frame" - | "embeddable"; + type: ToolType; } | { type: "custom"; customType: string } - ) & { lastActiveToolBeforeEraser?: LastActiveTool }, + ) & { lastActiveToolBeforeEraser?: ActiveTool | null }, ): AppState["activeTool"] => { if (data.type === "custom") { return { From e921bfb1aef6d3435fa8b209e371e1a399c220df Mon Sep 17 00:00:00 2001 From: DanielJGeiger <1852529+DanielJGeiger@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:17:22 -0500 Subject: [PATCH 10/18] feat: Export `iconFillColor()` (#6996) --- src/components/icons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 97fc25301..2d06d1073 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -13,7 +13,7 @@ import clsx from "clsx"; import { Theme } from "../element/types"; import { THEME } from "../constants"; -const iconFillColor = (theme: Theme) => "var(--icon-fill-color)"; +export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)"; const handlerColor = (theme: Theme) => theme === THEME.LIGHT ? oc.white : "#1e1e1e"; From 2e61926a6b0bf01a49db167979ea53a618b0ef2a Mon Sep 17 00:00:00 2001 From: Are Date: Thu, 5 Oct 2023 17:05:16 +0200 Subject: [PATCH 11/18] feat: initial Laser Pointer MVP (#6739) * feat: initial Laser pointer mvp * feat: add laser-pointer package and integrate it with collab * chore: fix yarn.lock * feat: update laser-pointer package, prevent panning from showing * feat: add laser pointer tool button when collaborating, migrate to official package * feat: reduce laser tool button size * update icon * fix icon & rotate * fix: lock zoom level * fix icon * add `selected` state, simplify and reduce api * set up pointer callbacks in viewMode if laser tool active * highlight extra-tools button if one of the nested tools active * add shortcut to laser pointer * feat: don't update paths if nothing changed * ensure we reset flag if no rAF scheduled * move `lastUpdate` to instance to optimize * return early * factor out into constants and add doc * skip iteration instead of exit * fix naming * feat: remove testing variable on window * destroy on editor unmount * fix incorrectly resetting `lastUpdate` in `stop()` --------- Co-authored-by: dwelle --- excalidraw-app/data/index.ts | 2 +- package.json | 1 + src/components/Actions.tsx | 37 ++- src/components/App.tsx | 51 ++- src/components/HelpDialog.tsx | 1 + src/components/LaserTool/LaserPathManager.ts | 293 ++++++++++++++++++ .../LaserTool/LaserPointerButton.tsx | 41 +++ src/components/LaserTool/LaserTool.tsx | 27 ++ .../LaserTool/LaserToolOverlay.scss | 20 ++ src/components/LayerUI.tsx | 21 ++ src/components/ToolIcon.scss | 5 + src/components/Toolbar.scss | 6 + src/components/icons.tsx | 19 ++ src/data/restore.ts | 1 + src/element/showSelectedShapeActions.ts | 3 +- src/locales/en.json | 1 + src/types.ts | 16 +- yarn.lock | 5 + 18 files changed, 531 insertions(+), 19 deletions(-) create mode 100644 src/components/LaserTool/LaserPathManager.ts create mode 100644 src/components/LaserTool/LaserPointerButton.tsx create mode 100644 src/components/LaserTool/LaserTool.tsx create mode 100644 src/components/LaserTool/LaserToolOverlay.scss diff --git a/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts index 3870ca37c..4dfb78017 100644 --- a/excalidraw-app/data/index.ts +++ b/excalidraw-app/data/index.ts @@ -107,7 +107,7 @@ export type SocketUpdateDataSource = { type: "MOUSE_LOCATION"; payload: { socketId: string; - pointer: { x: number; y: number }; + pointer: { x: number; y: number; tool: "pointer" | "laser" }; button: "down" | "up"; selectedElementIds: AppState["selectedElementIds"]; username: string; diff --git a/package.json b/package.json index a2a66b5c1..5ae2d1ff6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@braintree/sanitize-url": "6.0.2", + "@excalidraw/laser-pointer": "1.2.0", "@excalidraw/random-username": "1.0.0", "@radix-ui/react-popover": "1.0.3", "@radix-ui/react-tabs": "1.0.2", diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index a176ee5ab..cd5993097 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -31,7 +31,12 @@ import { import "./Actions.scss"; import DropdownMenu from "./dropdownMenu/DropdownMenu"; -import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons"; +import { + EmbedIcon, + extraToolsIcon, + frameToolIcon, + laserPointerToolIcon, +} from "./icons"; import { KEYS } from "../keys"; export const SelectedShapeActions = ({ @@ -222,6 +227,11 @@ export const ShapesSwitcher = ({ }) => { const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false); const device = useDevice(); + + const frameToolSelected = activeTool.type === "frame"; + const laserToolSelected = activeTool.type === "laser"; + const embeddableToolSelected = activeTool.type === "embeddable"; + return ( <> {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { @@ -313,7 +323,15 @@ export const ShapesSwitcher = ({ ) : ( setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)} title={t("toolBar.extraTools")} > @@ -331,7 +349,7 @@ export const ShapesSwitcher = ({ icon={frameToolIcon} shortcut={KEYS.F.toLocaleUpperCase()} data-testid="toolbar-frame" - selected={activeTool.type === "frame"} + selected={frameToolSelected} > {t("toolBar.frame")} @@ -341,10 +359,21 @@ export const ShapesSwitcher = ({ }} icon={EmbedIcon} data-testid="toolbar-embeddable" - selected={activeTool.type === "embeddable"} + selected={embeddableToolSelected} > {t("toolBar.embeddable")} + { + app.setActiveTool({ type: "laser" }); + }} + icon={laserPointerToolIcon} + data-testid="toolbar-laser" + selected={laserToolSelected} + shortcut={KEYS.K.toLocaleUpperCase()} + > + {t("toolBar.laser")} + )} diff --git a/src/components/App.tsx b/src/components/App.tsx index 8a9059557..737d2bed7 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -230,6 +230,7 @@ import { SidebarName, SidebarTabName, KeyboardModifiersObject, + CollaboratorPointer, ToolType, } from "../types"; import { @@ -368,6 +369,8 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; import { StaticCanvas, InteractiveCanvas } from "./canvases"; import { Renderer } from "../scene/Renderer"; import { ShapeCache } from "../scene/ShapeCache"; +import { LaserToolOverlay } from "./LaserTool/LaserTool"; +import { LaserPathManager } from "./LaserTool/LaserPathManager"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); @@ -497,6 +500,8 @@ class App extends React.Component { null; lastViewportPosition = { x: 0, y: 0 }; + laserPathManager: LaserPathManager = new LaserPathManager(this); + constructor(props: AppProps) { super(props); const defaultAppState = getDefaultAppState(); @@ -1205,12 +1210,14 @@ class App extends React.Component { !this.scene.getElementsIncludingDeleted().length } app={this} + isCollaborating={this.props.isCollaborating} > {this.props.children}
+ {selectedElements.length === 1 && !this.state.contextMenu && this.state.showHyperlinkPopup && ( @@ -1738,6 +1745,7 @@ class App extends React.Component { this.removeEventListeners(); this.scene.destroy(); this.library.destroy(); + this.laserPathManager.destroy(); ShapeCache.destroy(); SnapCache.destroy(); clearTimeout(touchTimeout); @@ -3052,6 +3060,15 @@ class App extends React.Component { } } + if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) { + if (this.state.activeTool.type === "laser") { + this.setActiveTool({ type: "selection" }); + } else { + this.setActiveTool({ type: "laser" }); + } + return; + } + if ( event[KEYS.CTRL_OR_CMD] && (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) @@ -4462,6 +4479,10 @@ class App extends React.Component { return; } + if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) { + return; + } + this.lastPointerDownEvent = event; this.setState({ @@ -4470,10 +4491,6 @@ class App extends React.Component { }); this.savePointer(event.clientX, event.clientY, "down"); - if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) { - return; - } - // only handle left mouse button or touch if ( event.button !== POINTER_BUTTON.MAIN && @@ -4564,6 +4581,11 @@ class App extends React.Component { setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); } else if (this.state.activeTool.type === "frame") { this.createFrameElementOnPointerDown(pointerDownState); + } else if (this.state.activeTool.type === "laser") { + this.laserPathManager.startPath( + pointerDownState.lastCoords.x, + pointerDownState.lastCoords.y, + ); } else if ( this.state.activeTool.type !== "eraser" && this.state.activeTool.type !== "hand" @@ -4587,7 +4609,7 @@ class App extends React.Component { lastPointerUp = onPointerUp; - if (!this.state.viewModeEnabled) { + if (!this.state.viewModeEnabled || this.state.activeTool.type === "laser") { window.addEventListener(EVENT.POINTER_MOVE, onPointerMove); window.addEventListener(EVENT.POINTER_UP, onPointerUp); window.addEventListener(EVENT.KEYDOWN, onKeyDown); @@ -5783,6 +5805,10 @@ class App extends React.Component { return; } + if (this.state.activeTool.type === "laser") { + this.laserPathManager.addPointToPath(pointerCoords.x, pointerCoords.y); + } + const [gridX, gridY] = getGridPoint( pointerCoords.x, pointerCoords.y, @@ -7029,6 +7055,11 @@ class App extends React.Component { : unbindLinearElements)(this.scene.getSelectedElements(this.state)); } + if (activeTool.type === "laser") { + this.laserPathManager.endPath(); + return; + } + if (!activeTool.locked && activeTool.type !== "freedraw") { resetCursor(this.interactiveCanvas); this.setState({ @@ -8273,15 +8304,21 @@ class App extends React.Component { if (!x || !y) { return; } - const pointer = viewportCoordsToSceneCoords( + const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( { clientX: x, clientY: y }, this.state, ); - if (isNaN(pointer.x) || isNaN(pointer.y)) { + if (isNaN(sceneX) || isNaN(sceneY)) { // sometimes the pointer goes off screen } + const pointer: CollaboratorPointer = { + x: sceneX, + y: sceneY, + tool: this.state.activeTool.type === "laser" ? "laser" : "pointer", + }; + this.props.onPointerUpdate?.({ pointer, button, diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 3954839ea..b27823fc5 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -165,6 +165,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { shortcuts={[KEYS.E, KEYS["0"]]} /> + (a + b) / 2; +function getSvgPathFromStroke(points: number[][], closed = true) { + const len = points.length; + + if (len < 4) { + return ``; + } + + let a = points[0]; + let b = points[1]; + const c = points[2]; + + let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed( + 2, + )},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average( + b[1], + c[1], + ).toFixed(2)} T`; + + for (let i = 2, max = len - 1; i < max; i++) { + a = points[i]; + b = points[i + 1]; + result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed( + 2, + )} `; + } + + if (closed) { + result += "Z"; + } + + return result; +} + +declare global { + interface Window { + LPM: LaserPathManager; + } +} + +function easeOutCubic(t: number) { + return 1 - Math.pow(1 - t, 3); +} + +function instantiateCollabolatorState(): CollabolatorState { + return { + currentPath: undefined, + finishedPaths: [], + lastPoint: [-10000, -10000], + svg: document.createElementNS("http://www.w3.org/2000/svg", "path"), + }; +} + +function instantiatePath() { + LaserPointer.constants.cornerDetectionMaxAngle = 70; + + return new LaserPointer({ + simplify: 0, + streamline: 0.4, + sizeMapping: (c) => { + const pt = DECAY_TIME; + const pl = DECAY_LENGTH; + const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt); + const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl; + + return Math.min(easeOutCubic(l), easeOutCubic(t)); + }, + }); +} + +type CollabolatorState = { + currentPath: LaserPointer | undefined; + finishedPaths: LaserPointer[]; + lastPoint: [number, number]; + svg: SVGPathElement; +}; + +export class LaserPathManager { + private ownState: CollabolatorState; + private collaboratorsState: Map = new Map(); + + private rafId: number | undefined; + private lastUpdate = 0; + private container: SVGSVGElement | undefined; + + constructor(private app: App) { + this.ownState = instantiateCollabolatorState(); + } + + destroy() { + this.stop(); + this.lastUpdate = 0; + this.ownState = instantiateCollabolatorState(); + this.collaboratorsState = new Map(); + } + + startPath(x: number, y: number) { + this.ownState.currentPath = instantiatePath(); + this.ownState.currentPath.addPoint([x, y, performance.now()]); + this.updatePath(this.ownState); + } + + addPointToPath(x: number, y: number) { + if (this.ownState.currentPath) { + this.ownState.currentPath?.addPoint([x, y, performance.now()]); + this.updatePath(this.ownState); + } + } + + endPath() { + if (this.ownState.currentPath) { + this.ownState.currentPath.close(); + this.ownState.finishedPaths.push(this.ownState.currentPath); + this.updatePath(this.ownState); + } + } + + private updatePath(state: CollabolatorState) { + this.lastUpdate = performance.now(); + + if (!this.isRunning) { + this.start(); + } + } + + private isRunning = false; + + start(svg?: SVGSVGElement) { + if (svg) { + this.container = svg; + this.container.appendChild(this.ownState.svg); + } + + this.stop(); + this.isRunning = true; + this.loop(); + } + + stop() { + this.isRunning = false; + if (this.rafId) { + cancelAnimationFrame(this.rafId); + } + this.rafId = undefined; + } + + loop() { + this.rafId = requestAnimationFrame(this.loop.bind(this)); + + this.updateCollabolatorsState(); + + if (performance.now() - this.lastUpdate < DECAY_TIME * 2) { + this.update(); + } else { + this.isRunning = false; + } + } + + draw(path: LaserPointer) { + const stroke = path + .getStrokeOutline(path.options.size / this.app.state.zoom.value) + .map(([x, y]) => { + const result = sceneCoordsToViewportCoords( + { sceneX: x, sceneY: y }, + this.app.state, + ); + + return [result.x, result.y]; + }); + + return getSvgPathFromStroke(stroke, true); + } + + updateCollabolatorsState() { + if (!this.container || !this.app.state.collaborators.size) { + return; + } + + for (const [key, collabolator] of this.app.state.collaborators.entries()) { + if (!this.collaboratorsState.has(key)) { + const state = instantiateCollabolatorState(); + this.container.appendChild(state.svg); + this.collaboratorsState.set(key, state); + + this.updatePath(state); + } + + const state = this.collaboratorsState.get(key)!; + + if (collabolator.pointer && collabolator.pointer.tool === "laser") { + if (collabolator.button === "down" && state.currentPath === undefined) { + state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y]; + state.currentPath = instantiatePath(); + state.currentPath.addPoint([ + collabolator.pointer.x, + collabolator.pointer.y, + performance.now(), + ]); + + this.updatePath(state); + } + + if (collabolator.button === "down" && state.currentPath !== undefined) { + if ( + collabolator.pointer.x !== state.lastPoint[0] || + collabolator.pointer.y !== state.lastPoint[1] + ) { + state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y]; + state.currentPath.addPoint([ + collabolator.pointer.x, + collabolator.pointer.y, + performance.now(), + ]); + + this.updatePath(state); + } + } + + if (collabolator.button === "up" && state.currentPath !== undefined) { + state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y]; + state.currentPath.addPoint([ + collabolator.pointer.x, + collabolator.pointer.y, + performance.now(), + ]); + state.currentPath.close(); + + state.finishedPaths.push(state.currentPath); + state.currentPath = undefined; + + this.updatePath(state); + } + } + } + } + + update() { + if (!this.container) { + return; + } + + for (const [key, state] of this.collaboratorsState.entries()) { + if (!this.app.state.collaborators.has(key)) { + state.svg.remove(); + this.collaboratorsState.delete(key); + continue; + } + + state.finishedPaths = state.finishedPaths.filter((path) => { + const lastPoint = path.originalPoints[path.originalPoints.length - 1]; + + return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME); + }); + + let paths = state.finishedPaths.map((path) => this.draw(path)).join(" "); + + if (state.currentPath) { + paths += ` ${this.draw(state.currentPath)}`; + } + + state.svg.setAttribute("d", paths); + state.svg.setAttribute("fill", getClientColor(key)); + } + + this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => { + const lastPoint = path.originalPoints[path.originalPoints.length - 1]; + + return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME); + }); + + let paths = this.ownState.finishedPaths + .map((path) => this.draw(path)) + .join(" "); + + if (this.ownState.currentPath) { + paths += ` ${this.draw(this.ownState.currentPath)}`; + } + + this.ownState.svg.setAttribute("d", paths); + this.ownState.svg.setAttribute("fill", "red"); + } +} diff --git a/src/components/LaserTool/LaserPointerButton.tsx b/src/components/LaserTool/LaserPointerButton.tsx new file mode 100644 index 000000000..dbb843293 --- /dev/null +++ b/src/components/LaserTool/LaserPointerButton.tsx @@ -0,0 +1,41 @@ +import "../ToolIcon.scss"; + +import clsx from "clsx"; +import { ToolButtonSize } from "../ToolButton"; +import { laserPointerToolIcon } from "../icons"; + +type LaserPointerIconProps = { + title?: string; + name?: string; + checked: boolean; + onChange?(): void; + isMobile?: boolean; +}; + +const DEFAULT_SIZE: ToolButtonSize = "small"; + +export const LaserPointerButton = (props: LaserPointerIconProps) => { + return ( + + ); +}; diff --git a/src/components/LaserTool/LaserTool.tsx b/src/components/LaserTool/LaserTool.tsx new file mode 100644 index 000000000..e93d72dfc --- /dev/null +++ b/src/components/LaserTool/LaserTool.tsx @@ -0,0 +1,27 @@ +import { useEffect, useRef } from "react"; +import { LaserPathManager } from "./LaserPathManager"; +import "./LaserToolOverlay.scss"; + +type LaserToolOverlayProps = { + manager: LaserPathManager; +}; + +export const LaserToolOverlay = ({ manager }: LaserToolOverlayProps) => { + const svgRef = useRef(null); + + useEffect(() => { + if (svgRef.current) { + manager.start(svgRef.current); + } + + return () => { + manager.stop(); + }; + }, [manager]); + + return ( +
+ +
+ ); +}; diff --git a/src/components/LaserTool/LaserToolOverlay.scss b/src/components/LaserTool/LaserToolOverlay.scss new file mode 100644 index 000000000..da874b452 --- /dev/null +++ b/src/components/LaserTool/LaserToolOverlay.scss @@ -0,0 +1,20 @@ +.excalidraw { + .LaserToolOverlay { + pointer-events: none; + width: 100vw; + height: 100vh; + position: fixed; + top: 0; + left: 0; + + z-index: 2; + + .LaserToolOverlayCanvas { + image-rendering: auto; + overflow: visible; + position: absolute; + top: 0; + left: 0; + } + } +} diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index d9d3d8ce4..59ac60a76 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -55,6 +55,7 @@ import "./Toolbar.scss"; import { mutateElement } from "../element/mutateElement"; import { ShapeCache } from "../scene/ShapeCache"; import Scene from "../scene/Scene"; +import { LaserPointerButton } from "./LaserTool/LaserPointerButton"; interface LayerUIProps { actionManager: ActionManager; @@ -77,6 +78,7 @@ interface LayerUIProps { renderWelcomeScreen: boolean; children?: React.ReactNode; app: AppClassProperties; + isCollaborating: boolean; } const DefaultMainMenu: React.FC<{ @@ -134,6 +136,7 @@ const LayerUI = ({ renderWelcomeScreen, children, app, + isCollaborating, }: LayerUIProps) => { const device = useDevice(); const tunnels = useInitializeTunnels(); @@ -288,6 +291,24 @@ const LayerUI = ({ /> + {isCollaborating && ( + + + app.setActiveTool({ type: "laser" }) + } + isMobile + /> + + )}
diff --git a/src/components/ToolIcon.scss b/src/components/ToolIcon.scss index 994ee6ba5..066f26d61 100644 --- a/src/components/ToolIcon.scss +++ b/src/components/ToolIcon.scss @@ -170,5 +170,10 @@ height: var(--lg-icon-size); } } + + .ToolIcon__LaserPointer .ToolIcon__icon { + width: var(--default-button-size); + height: var(--default-button-size); + } } } diff --git a/src/components/Toolbar.scss b/src/components/Toolbar.scss index 4bd20f7b3..aee50a144 100644 --- a/src/components/Toolbar.scss +++ b/src/components/Toolbar.scss @@ -28,6 +28,12 @@ box-shadow: 0 0 0 1px var(--button-active-border, var(--color-primary-darkest)) inset; } + + &--selected, + &--selected:hover { + background: var(--color-primary-light); + color: var(--color-primary); + } } .App-toolbar__extra-tools-dropdown { diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 2d06d1073..87059ce61 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1653,3 +1653,22 @@ export const frameToolIcon = createIcon( , tablerIconProps, ); + +export const laserPointerToolIcon = createIcon( + + + + , + + 20, +); diff --git a/src/data/restore.ts b/src/data/restore.ts index 9316cfe49..bda6818f6 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -67,6 +67,7 @@ export const AllowedExcalidrawActiveTools: Record< frame: true, embeddable: true, hand: true, + laser: false, }; export type RestoredDataState = { diff --git a/src/element/showSelectedShapeActions.ts b/src/element/showSelectedShapeActions.ts index cc42ec8d0..1fd47f683 100644 --- a/src/element/showSelectedShapeActions.ts +++ b/src/element/showSelectedShapeActions.ts @@ -12,6 +12,7 @@ export const showSelectedShapeActions = ( (appState.editingElement || (appState.activeTool.type !== "selection" && appState.activeTool.type !== "eraser" && - appState.activeTool.type !== "hand"))) || + appState.activeTool.type !== "hand" && + appState.activeTool.type !== "laser"))) || getSelectedElements(elements, appState).length), ); diff --git a/src/locales/en.json b/src/locales/en.json index c3a041f57..f2e6b601a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -236,6 +236,7 @@ "eraser": "Eraser", "frame": "Frame tool", "embeddable": "Web Embed", + "laser": "Laser pointer", "hand": "Hand (panning tool)", "extraTools": "More tools" }, diff --git a/src/types.ts b/src/types.ts index c3b0252e2..8b05ba40a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,10 +39,7 @@ import { Merge, ForwardRef, ValueOf } from "./utility-types"; export type Point = Readonly; export type Collaborator = { - pointer?: { - x: number; - y: number; - }; + pointer?: CollaboratorPointer; button?: "up" | "down"; selectedElementIds?: AppState["selectedElementIds"]; username?: string | null; @@ -58,6 +55,12 @@ export type Collaborator = { id?: string; }; +export type CollaboratorPointer = { + x: number; + y: number; + tool: "pointer" | "laser"; +}; + export type DataURL = string & { _brand: "DataURL" }; export type BinaryFileData = { @@ -98,7 +101,8 @@ export type ToolType = | "eraser" | "hand" | "frame" - | "embeddable"; + | "embeddable" + | "laser"; export type ActiveTool = | { @@ -389,7 +393,7 @@ export interface ExcalidrawProps { excalidrawRef?: ForwardRef; isCollaborating?: boolean; onPointerUpdate?: (payload: { - pointer: { x: number; y: number }; + pointer: { x: number; y: number; tool: "pointer" | "laser" }; button: "down" | "up"; pointersMap: Gesture["pointers"]; }) => void; diff --git a/yarn.lock b/yarn.lock index 166532f4e..8022634e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1522,6 +1522,11 @@ resolved "https://registry.yarnpkg.com/@excalidraw/eslint-config/-/eslint-config-1.0.3.tgz#2122ef7413ae77874ae9848ce0f1c6b3f0d8bbbd" integrity sha512-GemHNF5Z6ga0BWBSX7GJaNBUchLu6RwTcAB84eX1MeckRNhNasAsPCdelDlFalz27iS4RuYEQh0bPE8SRxJgbQ== +"@excalidraw/laser-pointer@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.2.0.tgz#cd34ea7d24b11743c726488cc1fcb28c161cacba" + integrity sha512-WjFFwLk9ahmKRKku7U0jqYpeM3fe9ZS1K43pfwPREHk4/FYU3iKDKVeS8m4tEAASnRlBt3hhLCBQLBF2uvgOnw== + "@excalidraw/prettier-config@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65" From a249f332a23aa4e902e06a9e41202b70588e9b6f Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 6 Oct 2023 12:00:35 +0200 Subject: [PATCH 12/18] fix: ensure we do not stop laser update prematurely (#7100) --- src/components/LaserTool/LaserPathManager.ts | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/LaserTool/LaserPathManager.ts b/src/components/LaserTool/LaserPathManager.ts index b36d7816c..2f0c63955 100644 --- a/src/components/LaserTool/LaserPathManager.ts +++ b/src/components/LaserTool/LaserPathManager.ts @@ -91,7 +91,7 @@ export class LaserPathManager { private collaboratorsState: Map = new Map(); private rafId: number | undefined; - private lastUpdate = 0; + private isDrawing = false; private container: SVGSVGElement | undefined; constructor(private app: App) { @@ -100,7 +100,7 @@ export class LaserPathManager { destroy() { this.stop(); - this.lastUpdate = 0; + this.isDrawing = false; this.ownState = instantiateCollabolatorState(); this.collaboratorsState = new Map(); } @@ -127,7 +127,7 @@ export class LaserPathManager { } private updatePath(state: CollabolatorState) { - this.lastUpdate = performance.now(); + this.isDrawing = true; if (!this.isRunning) { this.start(); @@ -160,7 +160,7 @@ export class LaserPathManager { this.updateCollabolatorsState(); - if (performance.now() - this.lastUpdate < DECAY_TIME * 2) { + if (this.isDrawing) { this.update(); } else { this.isRunning = false; @@ -250,6 +250,8 @@ export class LaserPathManager { return; } + let somePathsExist = false; + for (const [key, state] of this.collaboratorsState.entries()) { if (!this.app.state.collaborators.has(key)) { state.svg.remove(); @@ -269,6 +271,10 @@ export class LaserPathManager { paths += ` ${this.draw(state.currentPath)}`; } + if (paths.trim()) { + somePathsExist = true; + } + state.svg.setAttribute("d", paths); state.svg.setAttribute("fill", getClientColor(key)); } @@ -287,7 +293,17 @@ export class LaserPathManager { paths += ` ${this.draw(this.ownState.currentPath)}`; } + paths = paths.trim(); + + if (paths) { + somePathsExist = true; + } + this.ownState.svg.setAttribute("d", paths); this.ownState.svg.setAttribute("fill", "red"); + + if (!somePathsExist) { + this.isDrawing = false; + } } } From 03da9112cfaa08c2ad7616558355cfd538f940f4 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Mon, 9 Oct 2023 03:37:17 +0300 Subject: [PATCH 13/18] fix: update links to excalidraw-app (#7072) --- dev-docs/docs/@excalidraw/excalidraw/faq.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/faq.mdx b/dev-docs/docs/@excalidraw/excalidraw/faq.mdx index 4684d6c79..4274972ab 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/faq.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/faq.mdx @@ -2,7 +2,7 @@ ### Does this package support collaboration ? -No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same. +No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same. ### Turning off Aggressive Anti-Fingerprinting in Brave browser @@ -18,7 +18,7 @@ We strongly recommend turning it off. You can follow the steps below on how to d 2. Once opened, look for **Aggressively Block Fingerprinting** -![Aggresive block fingerprinting](../../assets/aggressive-block-fingerprint.png) +![Aggressive block fingerprinting](../../assets/aggressive-block-fingerprint.png) 3. Switch to **Block Fingerprinting** From f20ba90ffa2b3cf6c38844dd4bc6c8d816927f3b Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Mon, 9 Oct 2023 16:32:27 +0800 Subject: [PATCH 14/18] perf: improve element in frame check (#7124) --- src/frame.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/frame.ts b/src/frame.ts index 9ec98b9e9..1da9cfa10 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -707,6 +707,17 @@ export const isElementInFrame = ( : element; if (frame) { + // Perf improvement: + // For an element that's already in a frame, if it's not being dragged + // then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame. + // It has to be in its containing frame. + if ( + !appState.selectedElementIds[element.id] || + !appState.selectedElementsAreBeingDragged + ) { + return true; + } + if (_element.groupIds.length === 0) { return elementOverlapsWithFrame(_element, frame); } From 4ea079eb85a65ab06baac6b4a69be69cce98fdb0 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 9 Oct 2023 12:26:49 +0200 Subject: [PATCH 15/18] fix: regression from #6739 preventing redirect link in view mode (#7120) Co-authored-by: dwelle --- src/components/App.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 737d2bed7..d38fa630a 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4479,12 +4479,15 @@ class App extends React.Component { return; } + this.lastPointerDownEvent = event; + + // we must exit before we set `cursorButton` state and `savePointer` + // else it will send pointer state & laser pointer events in collab when + // panning if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) { return; } - this.lastPointerDownEvent = event; - this.setState({ lastPointerDownWith: event.pointerType, cursorButton: "down", From 2523fe82e3df7c7267c6026d971e6972055f8734 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 10 Oct 2023 13:55:55 +0200 Subject: [PATCH 16/18] feat: laser pointer improvements (#7128) --- src/components/App.tsx | 3 +++ src/utils.ts | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/components/App.tsx b/src/components/App.tsx index d38fa630a..f2c58165b 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1155,6 +1155,9 @@ class App extends React.Component { this.state.selectionElement || this.state.draggingElement || 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)) ? POINTER_EVENTS.disabled diff --git a/src/utils.ts b/src/utils.ts index 5acd1b664..65dfe1402 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,14 @@ import { isEraserActive, isHandToolActive } from "./appState"; import { ResolutionType } from "./utility-types"; import React from "react"; +const laserPointerCursorSVG = ` + +`; + +const laserPointerCursorDataURL = `data:${MIME_TYPES.svg},${encodeURIComponent( + `${laserPointerCursorSVG}`, +)}`; + let mockDateTime: string | null = null; export const setDateTimeForTests = (dateTime: string) => { @@ -467,6 +475,9 @@ export const setCursorForShape = ( // do nothing if image tool is selected which suggests there's // a image-preview set as the cursor // Ignore custom type as well and let host decide + } else if (appState.activeTool.type === "laser") { + const url = laserPointerCursorDataURL; + interactiveCanvas.style.cursor = `url(${url}), auto`; } else if (!["image", "custom"].includes(appState.activeTool.type)) { interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; } From 7ad02c359ae64bae28eb5b59b783c5d858cd12bc Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 10 Oct 2023 23:31:23 +0200 Subject: [PATCH 17/18] fix: memoize static canvas on `props.renderConfig` (#7131) --- src/components/canvases/StaticCanvas.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/canvases/StaticCanvas.tsx b/src/components/canvases/StaticCanvas.tsx index dfdf8b51e..38b9baade 100644 --- a/src/components/canvases/StaticCanvas.tsx +++ b/src/components/canvases/StaticCanvas.tsx @@ -114,11 +114,13 @@ const areEqual = ( return false; } - return isShallowEqual( - // asserting AppState because we're being passed the whole AppState - // but resolve to only the StaticCanvas-relevant props - getRelevantAppStateProps(prevProps.appState as AppState), - getRelevantAppStateProps(nextProps.appState as AppState), + return ( + isShallowEqual( + // asserting AppState because we're being passed the whole AppState + // but resolve to only the StaticCanvas-relevant props + getRelevantAppStateProps(prevProps.appState as AppState), + getRelevantAppStateProps(nextProps.appState as AppState), + ) && isShallowEqual(prevProps.renderConfig, nextProps.renderConfig) ); }; From 26ff3993bb0600cc3dbd77a8468820394a2204fc Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 11 Oct 2023 11:17:27 +0200 Subject: [PATCH 18/18] feat: better laser cursor for dark mode (#7132) --- src/actions/actionCanvas.tsx | 3 +- src/actions/actionFinalize.tsx | 3 +- src/actions/actionFrame.ts | 3 +- src/components/App.tsx | 10 ++-- src/cursor.ts | 103 +++++++++++++++++++++++++++++++++ src/element/embeddable.ts | 3 +- src/utils.ts | 96 +----------------------------- 7 files changed, 118 insertions(+), 103 deletions(-) create mode 100644 src/cursor.ts diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index 6531203ee..2ba584bc1 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -10,7 +10,7 @@ import { getNormalizedZoom } from "../scene"; import { centerScrollOn } from "../scene/scroll"; import { getStateForZoom } from "../scene/zoom"; import { AppState, NormalizedZoomValue } from "../types"; -import { getShortcutKey, setCursor, updateActiveTool } from "../utils"; +import { getShortcutKey, updateActiveTool } from "../utils"; import { register } from "./register"; import { Tooltip } from "../components/Tooltip"; import { newElementWith } from "../element/mutateElement"; @@ -21,6 +21,7 @@ import { } from "../appState"; import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; import { Bounds } from "../element/bounds"; +import { setCursor } from "../cursor"; export const actionChangeViewBackgroundColor = register({ name: "changeViewBackgroundColor", diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 4d422994c..a7c34c5ac 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -1,6 +1,6 @@ import { KEYS } from "../keys"; import { isInvisiblySmallElement } from "../element"; -import { updateActiveTool, resetCursor } from "../utils"; +import { updateActiveTool } from "../utils"; import { ToolButton } from "../components/ToolButton"; import { done } from "../components/icons"; import { t } from "../i18n"; @@ -15,6 +15,7 @@ import { } from "../element/binding"; import { isBindingElement, isLinearElement } from "../element/typeChecks"; import { AppState } from "../types"; +import { resetCursor } from "../cursor"; export const actionFinalize = register({ name: "finalize", diff --git a/src/actions/actionFrame.ts b/src/actions/actionFrame.ts index 339545f87..1266920ea 100644 --- a/src/actions/actionFrame.ts +++ b/src/actions/actionFrame.ts @@ -4,7 +4,8 @@ import { removeAllElementsFromFrame } from "../frame"; import { getFrameElements } from "../frame"; import { KEYS } from "../keys"; import { AppClassProperties, AppState } from "../types"; -import { setCursorForShape, updateActiveTool } from "../utils"; +import { updateActiveTool } from "../utils"; +import { setCursorForShape } from "../cursor"; import { register } from "./register"; const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => { diff --git a/src/components/App.tsx b/src/components/App.tsx index f2c58165b..4ad7b889e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -241,18 +241,14 @@ import { isInputLike, isToolIcon, isWritableElement, - resetCursor, resolvablePromise, sceneCoordsToViewportCoords, - setCursor, - setCursorForShape, tupleToCoors, viewportCoordsToSceneCoords, withBatchedUpdates, wrapEvent, withBatchedUpdatesThrottled, updateObject, - setEraserCursor, updateActiveTool, getShortcutKey, isTransparent, @@ -371,6 +367,12 @@ import { Renderer } from "../scene/Renderer"; import { ShapeCache } from "../scene/ShapeCache"; import { LaserToolOverlay } from "./LaserTool/LaserTool"; import { LaserPathManager } from "./LaserTool/LaserPathManager"; +import { + setEraserCursor, + setCursor, + resetCursor, + setCursorForShape, +} from "../cursor"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); diff --git a/src/cursor.ts b/src/cursor.ts new file mode 100644 index 000000000..364ce155c --- /dev/null +++ b/src/cursor.ts @@ -0,0 +1,103 @@ +import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants"; +import OpenColor from "open-color"; +import { AppState, DataURL } from "./types"; +import { isHandToolActive, isEraserActive } from "./appState"; + +const laserPointerCursorSVG_tag = ``; +const laserPointerCursorBackgroundSVG = ``; +const laserPointerCursorIconSVG = ``; + +const laserPointerCursorDataURL_lightMode = `data:${ + MIME_TYPES.svg +},${encodeURIComponent( + `${laserPointerCursorSVG_tag}${laserPointerCursorIconSVG}`, +)}`; +const laserPointerCursorDataURL_darkMode = `data:${ + MIME_TYPES.svg +},${encodeURIComponent( + `${laserPointerCursorSVG_tag}${laserPointerCursorBackgroundSVG}${laserPointerCursorIconSVG}`, +)}`; + +export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = ""; + } +}; + +export const setCursor = ( + interactiveCanvas: HTMLCanvasElement | null, + cursor: string, +) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = cursor; + } +}; + +let eraserCanvasCache: any; +let previewDataURL: string; +export const setEraserCursor = ( + interactiveCanvas: HTMLCanvasElement | null, + theme: AppState["theme"], +) => { + const cursorImageSizePx = 20; + + const drawCanvas = () => { + const isDarkTheme = theme === THEME.DARK; + eraserCanvasCache = document.createElement("canvas"); + eraserCanvasCache.theme = theme; + eraserCanvasCache.height = cursorImageSizePx; + eraserCanvasCache.width = cursorImageSizePx; + const context = eraserCanvasCache.getContext("2d")!; + context.lineWidth = 1; + context.beginPath(); + context.arc( + eraserCanvasCache.width / 2, + eraserCanvasCache.height / 2, + 5, + 0, + 2 * Math.PI, + ); + context.fillStyle = isDarkTheme ? OpenColor.black : OpenColor.white; + context.fill(); + context.strokeStyle = isDarkTheme ? OpenColor.white : OpenColor.black; + context.stroke(); + previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL; + }; + if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) { + drawCanvas(); + } + + setCursor( + interactiveCanvas, + `url(${previewDataURL}) ${cursorImageSizePx / 2} ${ + cursorImageSizePx / 2 + }, auto`, + ); +}; + +export const setCursorForShape = ( + interactiveCanvas: HTMLCanvasElement | null, + appState: Pick, +) => { + if (!interactiveCanvas) { + return; + } + if (appState.activeTool.type === "selection") { + resetCursor(interactiveCanvas); + } else if (isHandToolActive(appState)) { + interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB; + } else if (isEraserActive(appState)) { + setEraserCursor(interactiveCanvas, appState.theme); + // do nothing if image tool is selected which suggests there's + // a image-preview set as the cursor + // Ignore custom type as well and let host decide + } else if (appState.activeTool.type === "laser") { + const url = + appState.theme === THEME.LIGHT + ? laserPointerCursorDataURL_lightMode + : laserPointerCursorDataURL_darkMode; + interactiveCanvas.style.cursor = `url(${url}), auto`; + } else if (!["image", "custom"].includes(appState.activeTool.type)) { + interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; + } +}; diff --git a/src/element/embeddable.ts b/src/element/embeddable.ts index 80585bc72..4aa6f0fdc 100644 --- a/src/element/embeddable.ts +++ b/src/element/embeddable.ts @@ -2,7 +2,8 @@ import { register } from "../actions/register"; import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants"; import { t } from "../i18n"; import { ExcalidrawProps } from "../types"; -import { getFontString, setCursorForShape, updateActiveTool } from "../utils"; +import { getFontString, updateActiveTool } from "../utils"; +import { setCursorForShape } from "../cursor"; import { newTextElement } from "./newElement"; import { getContainerElement, wrapText } from "./textElement"; import { isEmbeddableElement } from "./typeChecks"; diff --git a/src/utils.ts b/src/utils.ts index 65dfe1402..f95139200 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,13 +1,9 @@ -import oc from "open-color"; import { COLOR_PALETTE } from "./colors"; import { - CURSOR_TYPE, DEFAULT_VERSION, EVENT, FONT_FAMILY, isDarwin, - MIME_TYPES, - THEME, WINDOWS_EMOJI_FALLBACK_FONT, } from "./constants"; import { @@ -15,20 +11,11 @@ import { FontString, NonDeletedExcalidrawElement, } from "./element/types"; -import { ActiveTool, AppState, DataURL, ToolType, Zoom } from "./types"; +import { ActiveTool, AppState, ToolType, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; -import { isEraserActive, isHandToolActive } from "./appState"; import { ResolutionType } from "./utility-types"; import React from "react"; -const laserPointerCursorSVG = ` - -`; - -const laserPointerCursorDataURL = `data:${MIME_TYPES.svg},${encodeURIComponent( - `${laserPointerCursorSVG}`, -)}`; - let mockDateTime: string | null = null; export const setDateTimeForTests = (dateTime: string) => { @@ -402,87 +389,6 @@ export const updateActiveTool = ( }; }; -export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => { - if (interactiveCanvas) { - interactiveCanvas.style.cursor = ""; - } -}; - -export const setCursor = ( - interactiveCanvas: HTMLCanvasElement | null, - cursor: string, -) => { - if (interactiveCanvas) { - interactiveCanvas.style.cursor = cursor; - } -}; - -let eraserCanvasCache: any; -let previewDataURL: string; -export const setEraserCursor = ( - interactiveCanvas: HTMLCanvasElement | null, - theme: AppState["theme"], -) => { - const cursorImageSizePx = 20; - - const drawCanvas = () => { - const isDarkTheme = theme === THEME.DARK; - eraserCanvasCache = document.createElement("canvas"); - eraserCanvasCache.theme = theme; - eraserCanvasCache.height = cursorImageSizePx; - eraserCanvasCache.width = cursorImageSizePx; - const context = eraserCanvasCache.getContext("2d")!; - context.lineWidth = 1; - context.beginPath(); - context.arc( - eraserCanvasCache.width / 2, - eraserCanvasCache.height / 2, - 5, - 0, - 2 * Math.PI, - ); - context.fillStyle = isDarkTheme ? oc.black : oc.white; - context.fill(); - context.strokeStyle = isDarkTheme ? oc.white : oc.black; - context.stroke(); - previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL; - }; - if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) { - drawCanvas(); - } - - setCursor( - interactiveCanvas, - `url(${previewDataURL}) ${cursorImageSizePx / 2} ${ - cursorImageSizePx / 2 - }, auto`, - ); -}; - -export const setCursorForShape = ( - interactiveCanvas: HTMLCanvasElement | null, - appState: Pick, -) => { - if (!interactiveCanvas) { - return; - } - if (appState.activeTool.type === "selection") { - resetCursor(interactiveCanvas); - } else if (isHandToolActive(appState)) { - interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB; - } else if (isEraserActive(appState)) { - setEraserCursor(interactiveCanvas, appState.theme); - // do nothing if image tool is selected which suggests there's - // a image-preview set as the cursor - // Ignore custom type as well and let host decide - } else if (appState.activeTool.type === "laser") { - const url = laserPointerCursorDataURL; - interactiveCanvas.style.cursor = `url(${url}), auto`; - } else if (!["image", "custom"].includes(appState.activeTool.type)) { - interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; - } -}; - export const isFullScreen = () => document.fullscreenElement?.nodeName === "HTML";