diff --git a/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts
index 3870ca37c5..4dfb780179 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 a2a66b5c1a..5ae2d1ff6c 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 a176ee5ab6..cd5993097e 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 8a90595579..737d2bed7f 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 3954839ea5..b27823fc55 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 0000000000..dbb8432938
--- /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 0000000000..e93d72dfc9
--- /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 0000000000..da874b452f
--- /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 d9d3d8ce45..59ac60a763 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 994ee6ba5f..066f26d61d 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 4bd20f7b3e..aee50a1446 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 2d06d1073e..87059ce617 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 9316cfe49c..bda6818f6f 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 cc42ec8d01..1fd47f6834 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 c3a041f579..f2e6b601a0 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 c3b0252e23..8b05ba40a7 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 166532f4e2..8022634e61 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"