From 2e61926a6b0bf01a49db167979ea53a618b0ef2a Mon Sep 17 00:00:00 2001 From: Are Date: Thu, 5 Oct 2023 17:05:16 +0200 Subject: [PATCH] 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"