mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01:00 
			
		
		
		
	Compare commits
	
		
			120 Commits
		
	
	
		
			v0.18.0
			...
			fix-zsvicz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a17090f455 | ||
| 
						 | 
					2df323a5c3 | ||
| 
						 | 
					df25de7e68 | ||
| 
						 | 
					a3763648fe | ||
| 
						 | 
					178eca5828 | ||
| 
						 | 
					cb33de25f4 | ||
| 
						 | 
					37ad85cbaf | ||
| 
						 | 
					d6a934ed19 | ||
| 
						 | 
					416da62138 | ||
| 
						 | 
					f38f381989 | ||
| 
						 | 
					e5e07260c6 | ||
| 
						 | 
					8492b144b0 | ||
| 
						 | 
					e46f038132 | ||
| 
						 | 
					678dff25ed | ||
| 
						 | 
					0cfa53b764 | ||
| 
						 | 
					cde46793f8 | ||
| 
						 | 
					2d127f8c22 | ||
| 
						 | 
					4eadb891f8 | ||
| 
						 | 
					258605d1d5 | ||
| 
						 | 
					c141500400 | ||
| 
						 | 
					8e27de2cdc | ||
| 
						 | 
					0a19c93509 | ||
| 
						 | 
					958597dfaa | ||
| 
						 | 
					058918f8e5 | ||
| 
						 | 
					3f194918e6 | ||
| 
						 | 
					93c92d13e9 | ||
| 
						 | 
					84e96e9393 | ||
| 
						 | 
					320af405e9 | ||
| 
						 | 
					60512f13d5 | ||
| 
						 | 
					f0458cc216 | ||
| 
						 | 
					9f3fdf5505 | ||
| 
						 | 
					f42e1ab64e | ||
| 
						 | 
					18808481fd | ||
| 
						 | 
					a7b64f02b3 | ||
| 
						 | 
					0d4abd1ddc | ||
| 
						 | 
					9e77373c81 | ||
| 
						 | 
					d108053351 | ||
| 
						 | 
					d4e85a9480 | ||
| 
						 | 
					08cd4c4f9a | ||
| 
						 | 
					469caadb87 | ||
| 
						 | 
					ca1a4f25e7 | ||
| 
						 | 
					56c05b3099 | ||
| 
						 | 
					6c0ff7fc5c | ||
| 
						 | 
					7cad3645a0 | ||
| 
						 | 
					5921ebc416 | ||
| 
						 | 
					864353be5f | ||
| 
						 | 
					db2911c6c4 | ||
| 
						 | 
					fc3e062074 | ||
| 
						 | 
					87c87a9fb1 | ||
| 
						 | 
					4dc205537c | ||
| 
						 | 
					cc571c4681 | ||
| 
						 | 
					14d512f321 | ||
| 
						 | 
					41c036e1a5 | ||
| 
						 | 
					91d36e9b81 | ||
| 
						 | 
					27522110df | ||
| 
						 | 
					712f267519 | ||
| 
						 | 
					41a7613dff | ||
| 
						 | 
					95d89a751a | ||
| 
						 | 
					6b5fb30d69 | ||
| 
						 | 
					d92a849038 | ||
| 
						 | 
					0a534f1bc6 | ||
| 
						 | 
					4ca5f53b1f | ||
| 
						 | 
					f7dcc893ea | ||
| 
						 | 
					4dfb8a3f8e | ||
| 
						 | 
					298812e1d0 | ||
| 
						 | 
					35bb449a4b | ||
| 
						 | 
					c4c064982f | ||
| 
						 | 
					51dbd4831b | ||
| 
						 | 
					7e41026812 | ||
| 
						 | 
					a8ebe514da | ||
| 
						 | 
					a30e1b25c6 | ||
| 
						 | 
					ff2ed5d26a | ||
| 
						 | 
					e058a08b33 | ||
| 
						 | 
					a306a909a0 | ||
| 
						 | 
					3dc54a724a | ||
| 
						 | 
					a7c61319dd | ||
| 
						 | 
					cec5232a7a | ||
| 
						 | 
					d4f70e9f31 | ||
| 
						 | 
					e19fd1332a | ||
| 
						 | 
					6e655cdb24 | ||
| 
						 | 
					192c4e7658 | ||
| 
						 | 
					195a743874 | ||
| 
						 | 
					4a60fe3d22 | ||
| 
						 | 
					2a0d15799c | ||
| 
						 | 
					a18b139a60 | ||
| 
						 | 
					1913599594 | ||
| 
						 | 
					debf2ad608 | ||
| 
						 | 
					8fb2f70414 | ||
| 
						 | 
					5fc13e4309 | ||
| 
						 | 
					b5d60973b7 | ||
| 
						 | 
					a5d6939826 | ||
| 
						 | 
					0cf36d6b30 | ||
| 
						 | 
					58f7d33d80 | ||
| 
						 | 
					6fe7de8020 | ||
| 
						 | 
					01304aac49 | ||
| 
						 | 
					dff69e9191 | ||
| 
						 | 
					6fc85022ae | ||
| 
						 | 
					e48b63a0ae | ||
| 
						 | 
					c2caf78e95 | ||
| 
						 | 
					ce267aa0d3 | ||
| 
						 | 
					6e47fadb59 | ||
| 
						 | 
					b3d5ba0567 | ||
| 
						 | 
					c79e892e55 | ||
| 
						 | 
					57a9e301d4 | ||
| 
						 | 
					7c58477382 | ||
| 
						 | 
					83fac6d0db | ||
| 
						 | 
					f2e8404c7b | ||
| 
						 | 
					d797c2e210 | ||
| 
						 | 
					0cd5a259ae | ||
| 
						 | 
					432a46ef9e | ||
| 
						 | 
					a18f059188 | ||
| 
						 | 
					ab89d4c16f | ||
| 
						 | 
					6c3a434f2a | ||
| 
						 | 
					e1bb59fb8f | ||
| 
						 | 
					77aca48c84 | ||
| 
						 | 
					58990b41ae | ||
| 
						 | 
					99d8bff175 | ||
| 
						 | 
					30983d801a | ||
| 
						 | 
					21ffaf4d76 | ||
| 
						 | 
					82b9a6b464 | 
@@ -1,3 +1,5 @@
 | 
			
		||||
MODE="development"
 | 
			
		||||
 | 
			
		||||
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
 | 
			
		||||
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
 | 
			
		||||
 | 
			
		||||
@@ -48,3 +50,6 @@ UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
 | 
			
		||||
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
 | 
			
		||||
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
 | 
			
		||||
HQIDAQAB'
 | 
			
		||||
 | 
			
		||||
# set to true in .env.development.local to disable the prevent unload dialog
 | 
			
		||||
VITE_APP_DISABLE_PREVENT_UNLOAD=
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
MODE="production"
 | 
			
		||||
 | 
			
		||||
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
 | 
			
		||||
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,21 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": ["@excalidraw/eslint-config", "react-app"],
 | 
			
		||||
  "rules": {
 | 
			
		||||
    "import/order": [
 | 
			
		||||
      "warn",
 | 
			
		||||
      {
 | 
			
		||||
        "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
 | 
			
		||||
        "pathGroups": [
 | 
			
		||||
          {
 | 
			
		||||
            "pattern": "@excalidraw/**",
 | 
			
		||||
            "group": "external",
 | 
			
		||||
            "position": "after"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "newlines-between": "always-and-inside-groups",
 | 
			
		||||
        "warnOnUnassignedImports": true
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "import/no-anonymous-default-export": "off",
 | 
			
		||||
    "no-restricted-globals": "off",
 | 
			
		||||
    "@typescript-eslint/consistent-type-imports": [
 | 
			
		||||
@@ -17,6 +32,12 @@
 | 
			
		||||
        "name": "jotai",
 | 
			
		||||
        "message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "react/jsx-no-target-blank": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "allowReferrer": true
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
# Project coding standards
 | 
			
		||||
 | 
			
		||||
## Generic Communication Guidelines
 | 
			
		||||
 | 
			
		||||
- Be succint and be aware that expansive generative AI answers are costly and slow
 | 
			
		||||
- Avoid providing explanations, trying to teach unless asked for, your chat partner is an expert
 | 
			
		||||
- Stop apologising if corrected, just provide the correct information or code
 | 
			
		||||
- Prefer code unless asked for explanation
 | 
			
		||||
- Stop summarizing what you've changed after modifications unless asked for
 | 
			
		||||
 | 
			
		||||
## TypeScript Guidelines
 | 
			
		||||
 | 
			
		||||
- Use TypeScript for all new code
 | 
			
		||||
- Where possible, prefer implementations without allocation
 | 
			
		||||
- When there is an option, opt for more performant solutions and trade RAM usage for less CPU cycles
 | 
			
		||||
- Prefer immutable data (const, readonly)
 | 
			
		||||
- Use optional chaining (?.) and nullish coalescing (??) operators
 | 
			
		||||
 | 
			
		||||
## React Guidelines
 | 
			
		||||
 | 
			
		||||
- Use functional components with hooks
 | 
			
		||||
- Follow the React hooks rules (no conditional hooks)
 | 
			
		||||
- Keep components small and focused
 | 
			
		||||
- Use CSS modules for component styling
 | 
			
		||||
 | 
			
		||||
## Naming Conventions
 | 
			
		||||
 | 
			
		||||
- Use PascalCase for component names, interfaces, and type aliases
 | 
			
		||||
- Use camelCase for variables, functions, and methods
 | 
			
		||||
- Use ALL_CAPS for constants
 | 
			
		||||
 | 
			
		||||
## Error Handling
 | 
			
		||||
 | 
			
		||||
- Use try/catch blocks for async operations
 | 
			
		||||
- Implement proper error boundaries in React components
 | 
			
		||||
- Always log errors with contextual information
 | 
			
		||||
 | 
			
		||||
## Testing
 | 
			
		||||
 | 
			
		||||
- Always attempt to fix #problems
 | 
			
		||||
- Always offer to run `yarn test:app` in the project root after modifications are complete and attempt fixing the issues reported
 | 
			
		||||
 | 
			
		||||
## Types
 | 
			
		||||
 | 
			
		||||
- Always include `packages/math/src/types.ts` in the context when your write math related code and always use the Point type instead of { x, y}
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/autorelease-excalidraw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/autorelease-excalidraw.yml
									
									
									
									
										vendored
									
									
								
							@@ -24,4 +24,4 @@ jobs:
 | 
			
		||||
      - name: Auto release
 | 
			
		||||
        run: |
 | 
			
		||||
          yarn add @actions/core -W
 | 
			
		||||
          yarn autorelease
 | 
			
		||||
          yarn release --tag=next --non-interactive
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								.github/workflows/autorelease-preview.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										55
									
								
								.github/workflows/autorelease-preview.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,55 +0,0 @@
 | 
			
		||||
name: Auto release excalidraw preview
 | 
			
		||||
on:
 | 
			
		||||
  issue_comment:
 | 
			
		||||
    types: [created, edited]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  Auto-release-excalidraw-preview:
 | 
			
		||||
    name: Auto release preview
 | 
			
		||||
    if: github.event.comment.body == '@excalibot trigger release' && github.event.issue.pull_request
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: React to release comment
 | 
			
		||||
        uses: peter-evans/create-or-update-comment@v1
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
 | 
			
		||||
          comment-id: ${{ github.event.comment.id }}
 | 
			
		||||
          reactions: "+1"
 | 
			
		||||
      - name: Get PR SHA
 | 
			
		||||
        id: sha
 | 
			
		||||
        uses: actions/github-script@v4
 | 
			
		||||
        with:
 | 
			
		||||
          result-encoding: string
 | 
			
		||||
          script: |
 | 
			
		||||
            const { owner, repo, number } = context.issue;
 | 
			
		||||
            const pr = await github.pulls.get({
 | 
			
		||||
              owner,
 | 
			
		||||
              repo,
 | 
			
		||||
              pull_number: number,
 | 
			
		||||
            });
 | 
			
		||||
            return pr.data.head.sha
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
        with:
 | 
			
		||||
          ref: ${{ steps.sha.outputs.result }}
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
      - name: Setup Node.js 18.x
 | 
			
		||||
        uses: actions/setup-node@v2
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18.x
 | 
			
		||||
      - name: Set up publish access
 | 
			
		||||
        run: |
 | 
			
		||||
          npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
 | 
			
		||||
        env:
 | 
			
		||||
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
 | 
			
		||||
      - name: Auto release preview
 | 
			
		||||
        id: "autorelease"
 | 
			
		||||
        run: |
 | 
			
		||||
          yarn add @actions/core -W
 | 
			
		||||
          yarn autorelease preview ${{ github.event.issue.number }}
 | 
			
		||||
      - name: Post comment post release
 | 
			
		||||
        if: always()
 | 
			
		||||
        uses: peter-evans/create-or-update-comment@v1
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
 | 
			
		||||
          issue-number: ${{ github.event.issue.number }}
 | 
			
		||||
          body: "@${{ github.event.comment.user.login }} ${{ steps.autorelease.outputs.result }}"
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/workflows/publish-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/publish-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,9 +17,14 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v3
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v3
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: excalidraw/excalidraw:latest
 | 
			
		||||
          platforms: linux/amd64, linux/arm64, linux/arm/v7
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -25,4 +25,5 @@ packages/excalidraw/types
 | 
			
		||||
coverage
 | 
			
		||||
dev-dist
 | 
			
		||||
html
 | 
			
		||||
meta*.json
 | 
			
		||||
meta*.json
 | 
			
		||||
.claude
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
# CLAUDE.md
 | 
			
		||||
 | 
			
		||||
## Project Structure
 | 
			
		||||
 | 
			
		||||
Excalidraw is a **monorepo** with a clear separation between the core library and the application:
 | 
			
		||||
 | 
			
		||||
- **`packages/excalidraw/`** - Main React component library published to npm as `@excalidraw/excalidraw`
 | 
			
		||||
- **`excalidraw-app/`** - Full-featured web application (excalidraw.com) that uses the library
 | 
			
		||||
- **`packages/`** - Core packages: `@excalidraw/common`, `@excalidraw/element`, `@excalidraw/math`, `@excalidraw/utils`
 | 
			
		||||
- **`examples/`** - Integration examples (NextJS, browser script)
 | 
			
		||||
 | 
			
		||||
## Development Workflow
 | 
			
		||||
 | 
			
		||||
1. **Package Development**: Work in `packages/*` for editor features
 | 
			
		||||
2. **App Development**: Work in `excalidraw-app/` for app-specific features
 | 
			
		||||
3. **Testing**: Always run `yarn test:update` before committing
 | 
			
		||||
4. **Type Safety**: Use `yarn test:typecheck` to verify TypeScript
 | 
			
		||||
 | 
			
		||||
## Development Commands
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
yarn test:typecheck  # TypeScript type checking
 | 
			
		||||
yarn test:update     # Run all tests (with snapshot updates)
 | 
			
		||||
yarn fix             # Auto-fix formatting and linting issues
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Architecture Notes
 | 
			
		||||
 | 
			
		||||
### Package System
 | 
			
		||||
 | 
			
		||||
- Uses Yarn workspaces for monorepo management
 | 
			
		||||
- Internal packages use path aliases (see `vitest.config.mts`)
 | 
			
		||||
- Build system uses esbuild for packages, Vite for the app
 | 
			
		||||
- TypeScript throughout with strict configuration
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:18 AS build
 | 
			
		||||
FROM --platform=${BUILDPLATFORM} node:18 AS build
 | 
			
		||||
 | 
			
		||||
WORKDIR /opt/node_app
 | 
			
		||||
 | 
			
		||||
@@ -6,13 +6,14 @@ COPY . .
 | 
			
		||||
 | 
			
		||||
# do not ignore optional dependencies:
 | 
			
		||||
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
 | 
			
		||||
RUN yarn --network-timeout 600000
 | 
			
		||||
RUN --mount=type=cache,target=/root/.cache/yarn \
 | 
			
		||||
    npm_config_target_arch=${TARGETARCH} yarn --network-timeout 600000
 | 
			
		||||
 | 
			
		||||
ARG NODE_ENV=production
 | 
			
		||||
 | 
			
		||||
RUN yarn build:app:docker
 | 
			
		||||
RUN npm_config_target_arch=${TARGETARCH} yarn build:app:docker
 | 
			
		||||
 | 
			
		||||
FROM nginx:1.27-alpine
 | 
			
		||||
FROM --platform=${TARGETPLATFORM} nginx:1.27-alpine
 | 
			
		||||
 | 
			
		||||
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,9 @@
 | 
			
		||||
  <a href="https://discord.gg/UexuTaE">
 | 
			
		||||
    <img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://deepwiki.com/excalidraw/excalidraw">
 | 
			
		||||
    <img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" />
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://twitter.com/excalidraw">
 | 
			
		||||
    <img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
 | 
			
		||||
  </a>
 | 
			
		||||
@@ -63,7 +66,7 @@ The Excalidraw editor (npm package) supports:
 | 
			
		||||
- 🏗️ Customizable.
 | 
			
		||||
- 📷 Image support.
 | 
			
		||||
- 😀 Shape libraries support.
 | 
			
		||||
- 👅 Localization (i18n) support.
 | 
			
		||||
- 🌐 Localization (i18n) support.
 | 
			
		||||
- 🖼️ Export to PNG, SVG & clipboard.
 | 
			
		||||
- 💾 Open format - export drawings as an `.excalidraw` json file.
 | 
			
		||||
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
 | 
			
		||||
 | 
			
		||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
 | 
			
		||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should be a valid React Node.
 | 
			
		||||
 | 
			
		||||
**Usage**
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +25,7 @@ function App() {
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will only for `Desktop` devices.
 | 
			
		||||
This will only work for `Desktop` devices.
 | 
			
		||||
 | 
			
		||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
 | 
			
		||||
 | 
			
		||||
@@ -65,4 +65,4 @@ const App = () => (
 | 
			
		||||
// Need to render when code is span across multiple components
 | 
			
		||||
// in Live Code blocks editor
 | 
			
		||||
render(<App />);
 | 
			
		||||
```
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -363,13 +363,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
 | 
			
		||||
```ts
 | 
			
		||||
(
 | 
			
		||||
  tool: (
 | 
			
		||||
    | (
 | 
			
		||||
        | { type: Exclude<ToolType, "image"> }
 | 
			
		||||
        | {
 | 
			
		||||
            type: Extract<ToolType, "image">;
 | 
			
		||||
            insertOnCanvasDirectly?: boolean;
 | 
			
		||||
          }
 | 
			
		||||
      )
 | 
			
		||||
    | { type: ToolType }
 | 
			
		||||
    | { type: "custom"; customType: string }
 | 
			
		||||
  ) & { locked?: boolean },
 | 
			
		||||
) => {};
 | 
			
		||||
@@ -377,7 +371,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
 | 
			
		||||
 | 
			
		||||
| Name | Type | Default | Description |
 | 
			
		||||
| --- | --- | --- | --- |
 | 
			
		||||
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
 | 
			
		||||
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool |
 | 
			
		||||
| `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
 | 
			
		||||
 | 
			
		||||
## setCursor
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
All `props` are _optional_.
 | 
			
		||||
 | 
			
		||||
| Name | Type | Default | Description |
 | 
			
		||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
 | 
			
		||||
| --- | --- | --- | --- |
 | 
			
		||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
 | 
			
		||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
 | 
			
		||||
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
 | 
			
		||||
@@ -13,7 +13,7 @@ All `props` are _optional_.
 | 
			
		||||
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
 | 
			
		||||
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
 | 
			
		||||
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
 | 
			
		||||
| [`generateLinkForSelection`](#generateLinkForSelection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
 | 
			
		||||
| [`generateLinkForSelection`](#generatelinkforselection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
 | 
			
		||||
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
 | 
			
		||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
 | 
			
		||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
 | 
			
		||||
@@ -29,8 +29,9 @@ All `props` are _optional_.
 | 
			
		||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
 | 
			
		||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
 | 
			
		||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
 | 
			
		||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
 | 
			
		||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
 | 
			
		||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
 | 
			
		||||
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
 | 
			
		||||
 | 
			
		||||
### Storing custom data on Excalidraw elements
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,32 +28,12 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
 | 
			
		||||
 | 
			
		||||
## Releasing
 | 
			
		||||
 | 
			
		||||
### Create a test release
 | 
			
		||||
 | 
			
		||||
You can create a test release by posting the below comment in your pull request:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
@excalibot trigger release
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Once the version is released `@excalibot` will post a comment with the release version.
 | 
			
		||||
 | 
			
		||||
### Creating a production release
 | 
			
		||||
 | 
			
		||||
To release the next stable version follow the below steps:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
yarn prerelease:excalidraw
 | 
			
		||||
yarn release --tag=latest --version=0.19.0
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
 | 
			
		||||
 | 
			
		||||
The next step is to run the `release` script:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
yarn release:excalidraw
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will publish the package.
 | 
			
		||||
 | 
			
		||||
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.
 | 
			
		||||
You will need to pass the `latest` tag with `version` for which you want to create the release. This will make the changes needed before publishing the packages into NPM, like updating dependencies of all `@excalidraw/*` packages, generating new entries in `CHANGELOG.md` and more.
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,8 @@ If you want to only import `Excalidraw` component you can do :point_down:
 | 
			
		||||
 | 
			
		||||
```jsx showLineNumbers
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import "@excalidraw/excalidraw/index.css";
 | 
			
		||||
 | 
			
		||||
const Excalidraw = dynamic(
 | 
			
		||||
  async () => (await import("@excalidraw/excalidraw")).Excalidraw,
 | 
			
		||||
  {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import styles from "./styles.module.css";
 | 
			
		||||
 | 
			
		||||
const FeatureList = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import styles from "./styles.module.css";
 | 
			
		||||
 | 
			
		||||
type FeatureItem = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import Layout from "@theme/Layout";
 | 
			
		||||
import Link from "@docusaurus/Link";
 | 
			
		||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
 | 
			
		||||
import styles from "./index.module.css";
 | 
			
		||||
import HomepageFeatures from "@site/src/components/Homepage";
 | 
			
		||||
import Layout from "@theme/Layout";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import styles from "./index.module.css";
 | 
			
		||||
 | 
			
		||||
function HomepageHeader() {
 | 
			
		||||
  const { siteConfig } = useDocusaurusContext();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
// Import the original mapper
 | 
			
		||||
import MDXComponents from "@theme-original/MDXComponents";
 | 
			
		||||
import Highlight from "@site/src/components/Highlight";
 | 
			
		||||
import MDXComponents from "@theme-original/MDXComponents";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  // Re-use the default mapping
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ const ExcalidrawScope = {
 | 
			
		||||
  initialData,
 | 
			
		||||
  useI18n: ExcalidrawComp.useI18n,
 | 
			
		||||
  convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements,
 | 
			
		||||
  CaptureUpdateAction: ExcalidrawComp.CaptureUpdateAction,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ExcalidrawScope;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,8 @@
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
 | 
			
		||||
    "build:packages": "yarn --cwd ../../ build:packages",
 | 
			
		||||
    "build:workspace": "yarn build:packages && yarn copy:assets",
 | 
			
		||||
    "copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public",
 | 
			
		||||
    "dev": "yarn build:workspace && next dev -p 3005",
 | 
			
		||||
    "build": "yarn build:workspace && next build",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import Script from "next/script";
 | 
			
		||||
 | 
			
		||||
import "../common.scss";
 | 
			
		||||
 | 
			
		||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import * as excalidrawLib from "@excalidraw/excalidraw";
 | 
			
		||||
import { Excalidraw } from "@excalidraw/excalidraw";
 | 
			
		||||
import App from "../../with-script-in-browser/components/ExampleApp";
 | 
			
		||||
 | 
			
		||||
import "@excalidraw/excalidraw/index.css";
 | 
			
		||||
 | 
			
		||||
import App from "../../with-script-in-browser/components/ExampleApp";
 | 
			
		||||
 | 
			
		||||
const ExcalidrawWrapper: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
 | 
			
		||||
import "../common.scss";
 | 
			
		||||
 | 
			
		||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
 | 
			
		||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@
 | 
			
		||||
  transform: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.excalidraw .panelColumn {
 | 
			
		||||
.excalidraw .selected-shape-actions {
 | 
			
		||||
  text-align: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
import React, {
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useState,
 | 
			
		||||
@@ -6,13 +7,24 @@ import React, {
 | 
			
		||||
  Children,
 | 
			
		||||
  cloneElement,
 | 
			
		||||
} from "react";
 | 
			
		||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
 | 
			
		||||
 | 
			
		||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
 | 
			
		||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
 | 
			
		||||
import type {
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
  Theme,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  ExcalidrawImperativeAPI,
 | 
			
		||||
  ExcalidrawInitialDataState,
 | 
			
		||||
  Gesture,
 | 
			
		||||
  LibraryItems,
 | 
			
		||||
  PointerDownState as ExcalidrawPointerDownState,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
 | 
			
		||||
import type { ResolvablePromise } from "../utils";
 | 
			
		||||
import initialData from "../initialData";
 | 
			
		||||
import {
 | 
			
		||||
  resolvablePromise,
 | 
			
		||||
  distance2d,
 | 
			
		||||
@@ -23,25 +35,12 @@ import {
 | 
			
		||||
 | 
			
		||||
import CustomFooter from "./CustomFooter";
 | 
			
		||||
import MobileFooter from "./MobileFooter";
 | 
			
		||||
import initialData from "../initialData";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  ExcalidrawImperativeAPI,
 | 
			
		||||
  ExcalidrawInitialDataState,
 | 
			
		||||
  Gesture,
 | 
			
		||||
  LibraryItems,
 | 
			
		||||
  PointerDownState as ExcalidrawPointerDownState,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import type {
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
  Theme,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
 | 
			
		||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
 | 
			
		||||
 | 
			
		||||
import "./ExampleApp.scss";
 | 
			
		||||
 | 
			
		||||
import type { ResolvablePromise } from "../utils";
 | 
			
		||||
 | 
			
		||||
type Comment = {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
@@ -105,6 +104,7 @@ export default function ExampleApp({
 | 
			
		||||
  const [viewModeEnabled, setViewModeEnabled] = useState(false);
 | 
			
		||||
  const [zenModeEnabled, setZenModeEnabled] = useState(false);
 | 
			
		||||
  const [gridModeEnabled, setGridModeEnabled] = useState(false);
 | 
			
		||||
  const [renderScrollbars, setRenderScrollbars] = useState(false);
 | 
			
		||||
  const [blobUrl, setBlobUrl] = useState<string>("");
 | 
			
		||||
  const [canvasUrl, setCanvasUrl] = useState<string>("");
 | 
			
		||||
  const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
 | 
			
		||||
@@ -193,6 +193,7 @@ export default function ExampleApp({
 | 
			
		||||
        }) => setPointerData(payload),
 | 
			
		||||
        viewModeEnabled,
 | 
			
		||||
        zenModeEnabled,
 | 
			
		||||
        renderScrollbars,
 | 
			
		||||
        gridModeEnabled,
 | 
			
		||||
        theme,
 | 
			
		||||
        name: "Custom name of drawing",
 | 
			
		||||
@@ -711,6 +712,14 @@ export default function ExampleApp({
 | 
			
		||||
            />
 | 
			
		||||
            Grid mode
 | 
			
		||||
          </label>
 | 
			
		||||
          <label>
 | 
			
		||||
            <input
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              checked={renderScrollbars}
 | 
			
		||||
              onChange={() => setRenderScrollbars(!renderScrollbars)}
 | 
			
		||||
            />
 | 
			
		||||
            Render scrollbars
 | 
			
		||||
          </label>
 | 
			
		||||
          <label>
 | 
			
		||||
            <input
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
 | 
			
		||||
import CustomFooter from "./CustomFooter";
 | 
			
		||||
 | 
			
		||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
 | 
			
		||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import CustomFooter from "./CustomFooter";
 | 
			
		||||
 | 
			
		||||
const MobileFooter = ({
 | 
			
		||||
  excalidrawAPI,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
 | 
			
		||||
import "./ExampleSidebar.scss";
 | 
			
		||||
 | 
			
		||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import App from "./components/ExampleApp";
 | 
			
		||||
import React, { StrictMode } from "react";
 | 
			
		||||
import { createRoot } from "react-dom/client";
 | 
			
		||||
 | 
			
		||||
import "@excalidraw/excalidraw/index.css";
 | 
			
		||||
 | 
			
		||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
 | 
			
		||||
 | 
			
		||||
import "@excalidraw/excalidraw/index.css";
 | 
			
		||||
import App from "./components/ExampleApp";
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,8 @@
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "build:preview": "yarn build && vite preview --port 5002",
 | 
			
		||||
    "build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
 | 
			
		||||
    "preview": "vite preview --port 5002",
 | 
			
		||||
    "build:preview": "yarn build && yarn preview",
 | 
			
		||||
    "build:packages": "yarn --cwd ../../ build:packages"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { unstable_batchedUpdates } from "react-dom";
 | 
			
		||||
import { fileOpen as _fileOpen } from "browser-fs-access";
 | 
			
		||||
import { MIME_TYPES } from "@excalidraw/excalidraw";
 | 
			
		||||
import { fileOpen as _fileOpen } from "browser-fs-access";
 | 
			
		||||
import { unstable_batchedUpdates } from "react-dom";
 | 
			
		||||
 | 
			
		||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "outputDirectory": "dist",
 | 
			
		||||
  "installCommand": "yarn install",
 | 
			
		||||
  "buildCommand": "yarn build:package && yarn build"
 | 
			
		||||
  "buildCommand": "yarn build:packages && yarn build"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,3 @@
 | 
			
		||||
import polyfill from "@excalidraw/excalidraw/polyfill";
 | 
			
		||||
import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
 | 
			
		||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
 | 
			
		||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
 | 
			
		||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
 | 
			
		||||
import {
 | 
			
		||||
  APP_NAME,
 | 
			
		||||
  EVENT,
 | 
			
		||||
  THEME,
 | 
			
		||||
  TITLE_TIMEOUT,
 | 
			
		||||
  VERSION_TIMEOUT,
 | 
			
		||||
} from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
 | 
			
		||||
import type {
 | 
			
		||||
  FileId,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import {
 | 
			
		||||
  Excalidraw,
 | 
			
		||||
  LiveCollaborationTrigger,
 | 
			
		||||
@@ -26,15 +5,23 @@ import {
 | 
			
		||||
  CaptureUpdateAction,
 | 
			
		||||
  reconcileElements,
 | 
			
		||||
} from "@excalidraw/excalidraw";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  ExcalidrawImperativeAPI,
 | 
			
		||||
  BinaryFiles,
 | 
			
		||||
  ExcalidrawInitialDataState,
 | 
			
		||||
  UIAppState,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import type { ResolvablePromise } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
 | 
			
		||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
 | 
			
		||||
import {
 | 
			
		||||
  CommandPalette,
 | 
			
		||||
  DEFAULT_CATEGORIES,
 | 
			
		||||
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
 | 
			
		||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
 | 
			
		||||
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
 | 
			
		||||
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
 | 
			
		||||
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
 | 
			
		||||
import Trans from "@excalidraw/excalidraw/components/Trans";
 | 
			
		||||
import {
 | 
			
		||||
  APP_NAME,
 | 
			
		||||
  EVENT,
 | 
			
		||||
  THEME,
 | 
			
		||||
  TITLE_TIMEOUT,
 | 
			
		||||
  VERSION_TIMEOUT,
 | 
			
		||||
  debounce,
 | 
			
		||||
  getVersion,
 | 
			
		||||
  getFrame,
 | 
			
		||||
@@ -42,75 +29,14 @@ import {
 | 
			
		||||
  preventUnload,
 | 
			
		||||
  resolvablePromise,
 | 
			
		||||
  isRunningInIframe,
 | 
			
		||||
} from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import {
 | 
			
		||||
  FIREBASE_STORAGE_PREFIXES,
 | 
			
		||||
  isExcalidrawPlusSignedUser,
 | 
			
		||||
  STORAGE_KEYS,
 | 
			
		||||
  SYNC_BROWSER_TABS_TIMEOUT,
 | 
			
		||||
} from "./app_constants";
 | 
			
		||||
import type { CollabAPI } from "./collab/Collab";
 | 
			
		||||
import Collab, {
 | 
			
		||||
  collabAPIAtom,
 | 
			
		||||
  isCollaboratingAtom,
 | 
			
		||||
  isOfflineAtom,
 | 
			
		||||
} from "./collab/Collab";
 | 
			
		||||
import {
 | 
			
		||||
  exportToBackend,
 | 
			
		||||
  getCollaborationLinkData,
 | 
			
		||||
  isCollaborationLink,
 | 
			
		||||
  loadScene,
 | 
			
		||||
} from "./data";
 | 
			
		||||
import {
 | 
			
		||||
  importFromLocalStorage,
 | 
			
		||||
  importUsernameFromLocalStorage,
 | 
			
		||||
} from "./data/localStorage";
 | 
			
		||||
import CustomStats from "./CustomStats";
 | 
			
		||||
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
 | 
			
		||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
 | 
			
		||||
import {
 | 
			
		||||
  ExportToExcalidrawPlus,
 | 
			
		||||
  exportToExcalidrawPlus,
 | 
			
		||||
} from "./components/ExportToExcalidrawPlus";
 | 
			
		||||
import { updateStaleImageStatuses } from "./data/FileManager";
 | 
			
		||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
 | 
			
		||||
import { loadFilesFromFirebase } from "./data/firebase";
 | 
			
		||||
import {
 | 
			
		||||
  LibraryIndexedDBAdapter,
 | 
			
		||||
  LibraryLocalStorageMigrationAdapter,
 | 
			
		||||
  LocalData,
 | 
			
		||||
} from "./data/LocalData";
 | 
			
		||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import {
 | 
			
		||||
  parseLibraryTokensFromUrl,
 | 
			
		||||
  useHandleLibrary,
 | 
			
		||||
} from "@excalidraw/excalidraw/data/library";
 | 
			
		||||
import { AppMainMenu } from "./components/AppMainMenu";
 | 
			
		||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
 | 
			
		||||
import { AppFooter } from "./components/AppFooter";
 | 
			
		||||
import {
 | 
			
		||||
  Provider,
 | 
			
		||||
  useAtom,
 | 
			
		||||
  useAtomValue,
 | 
			
		||||
  useAtomWithInitialValue,
 | 
			
		||||
  appJotaiStore,
 | 
			
		||||
} from "./app-jotai";
 | 
			
		||||
  isDevEnv,
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
import polyfill from "@excalidraw/excalidraw/polyfill";
 | 
			
		||||
import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
 | 
			
		||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
 | 
			
		||||
import "./index.scss";
 | 
			
		||||
import type { ResolutionType } from "@excalidraw/excalidraw/utility-types";
 | 
			
		||||
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
 | 
			
		||||
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
 | 
			
		||||
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
 | 
			
		||||
import Trans from "@excalidraw/excalidraw/components/Trans";
 | 
			
		||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
 | 
			
		||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
 | 
			
		||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
 | 
			
		||||
import {
 | 
			
		||||
  CommandPalette,
 | 
			
		||||
  DEFAULT_CATEGORIES,
 | 
			
		||||
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
 | 
			
		||||
import {
 | 
			
		||||
  GithubIcon,
 | 
			
		||||
  XBrandIcon,
 | 
			
		||||
@@ -121,6 +47,83 @@ import {
 | 
			
		||||
  share,
 | 
			
		||||
  youtubeIcon,
 | 
			
		||||
} from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import { isElementLink } from "@excalidraw/element";
 | 
			
		||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
 | 
			
		||||
import { newElementWith } from "@excalidraw/element";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/element";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import {
 | 
			
		||||
  parseLibraryTokensFromUrl,
 | 
			
		||||
  useHandleLibrary,
 | 
			
		||||
} from "@excalidraw/excalidraw/data/library";
 | 
			
		||||
 | 
			
		||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
 | 
			
		||||
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
 | 
			
		||||
import type {
 | 
			
		||||
  FileId,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  ExcalidrawImperativeAPI,
 | 
			
		||||
  BinaryFiles,
 | 
			
		||||
  ExcalidrawInitialDataState,
 | 
			
		||||
  UIAppState,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import type { ResolutionType } from "@excalidraw/common/utility-types";
 | 
			
		||||
import type { ResolvablePromise } from "@excalidraw/common/utils";
 | 
			
		||||
 | 
			
		||||
import CustomStats from "./CustomStats";
 | 
			
		||||
import {
 | 
			
		||||
  Provider,
 | 
			
		||||
  useAtom,
 | 
			
		||||
  useAtomValue,
 | 
			
		||||
  useAtomWithInitialValue,
 | 
			
		||||
  appJotaiStore,
 | 
			
		||||
} from "./app-jotai";
 | 
			
		||||
import {
 | 
			
		||||
  FIREBASE_STORAGE_PREFIXES,
 | 
			
		||||
  isExcalidrawPlusSignedUser,
 | 
			
		||||
  STORAGE_KEYS,
 | 
			
		||||
  SYNC_BROWSER_TABS_TIMEOUT,
 | 
			
		||||
} from "./app_constants";
 | 
			
		||||
import Collab, {
 | 
			
		||||
  collabAPIAtom,
 | 
			
		||||
  isCollaboratingAtom,
 | 
			
		||||
  isOfflineAtom,
 | 
			
		||||
} from "./collab/Collab";
 | 
			
		||||
import { AppFooter } from "./components/AppFooter";
 | 
			
		||||
import { AppMainMenu } from "./components/AppMainMenu";
 | 
			
		||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
 | 
			
		||||
import {
 | 
			
		||||
  ExportToExcalidrawPlus,
 | 
			
		||||
  exportToExcalidrawPlus,
 | 
			
		||||
} from "./components/ExportToExcalidrawPlus";
 | 
			
		||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  exportToBackend,
 | 
			
		||||
  getCollaborationLinkData,
 | 
			
		||||
  isCollaborationLink,
 | 
			
		||||
  loadScene,
 | 
			
		||||
} from "./data";
 | 
			
		||||
 | 
			
		||||
import { updateStaleImageStatuses } from "./data/FileManager";
 | 
			
		||||
import {
 | 
			
		||||
  importFromLocalStorage,
 | 
			
		||||
  importUsernameFromLocalStorage,
 | 
			
		||||
} from "./data/localStorage";
 | 
			
		||||
 | 
			
		||||
import { loadFilesFromFirebase } from "./data/firebase";
 | 
			
		||||
import {
 | 
			
		||||
  LibraryIndexedDBAdapter,
 | 
			
		||||
  LibraryLocalStorageMigrationAdapter,
 | 
			
		||||
  LocalData,
 | 
			
		||||
} from "./data/LocalData";
 | 
			
		||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
 | 
			
		||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
 | 
			
		||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
 | 
			
		||||
import { useHandleAppTheme } from "./useHandleAppTheme";
 | 
			
		||||
import { getPreferredLanguage } from "./app-language/language-detector";
 | 
			
		||||
import { useAppLangCode } from "./app-language/language-state";
 | 
			
		||||
@@ -131,7 +134,10 @@ import DebugCanvas, {
 | 
			
		||||
} from "./components/DebugCanvas";
 | 
			
		||||
import { AIComponents } from "./components/AI";
 | 
			
		||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
 | 
			
		||||
import { isElementLink } from "@excalidraw/excalidraw/element/elementLink";
 | 
			
		||||
 | 
			
		||||
import "./index.scss";
 | 
			
		||||
 | 
			
		||||
import type { CollabAPI } from "./collab/Collab";
 | 
			
		||||
 | 
			
		||||
polyfill();
 | 
			
		||||
 | 
			
		||||
@@ -377,7 +383,7 @@ const ExcalidrawWrapper = () => {
 | 
			
		||||
  const [, forceRefresh] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (import.meta.env.DEV) {
 | 
			
		||||
    if (isDevEnv()) {
 | 
			
		||||
      const debugState = loadSavedDebugState();
 | 
			
		||||
 | 
			
		||||
      if (debugState.enabled && !window.visualDebug) {
 | 
			
		||||
@@ -602,7 +608,13 @@ const ExcalidrawWrapper = () => {
 | 
			
		||||
          excalidrawAPI.getSceneElements(),
 | 
			
		||||
        )
 | 
			
		||||
      ) {
 | 
			
		||||
        preventUnload(event);
 | 
			
		||||
        if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
 | 
			
		||||
          preventUnload(event);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.warn(
 | 
			
		||||
            "preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,21 @@
 | 
			
		||||
import { Stats } from "@excalidraw/excalidraw";
 | 
			
		||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_VERSION,
 | 
			
		||||
  debounce,
 | 
			
		||||
  getVersion,
 | 
			
		||||
  nFormatter,
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { debounce, getVersion, nFormatter } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
 | 
			
		||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
 | 
			
		||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  getElementsStorageSize,
 | 
			
		||||
  getTotalStorageSize,
 | 
			
		||||
} from "./data/localStorage";
 | 
			
		||||
import { DEFAULT_VERSION } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
 | 
			
		||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
import { Stats } from "@excalidraw/excalidraw";
 | 
			
		||||
 | 
			
		||||
type StorageSizes = { scene: number; total: number };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
 | 
			
		||||
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
 | 
			
		||||
import { useLayoutEffect, useRef } from "react";
 | 
			
		||||
import { STORAGE_KEYS } from "./app_constants";
 | 
			
		||||
import { LocalData } from "./data/LocalData";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  FileId,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
 | 
			
		||||
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
 | 
			
		||||
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
 | 
			
		||||
 | 
			
		||||
import { STORAGE_KEYS } from "./app_constants";
 | 
			
		||||
import { LocalData } from "./data/LocalData";
 | 
			
		||||
 | 
			
		||||
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { useSetAtom } from "../app-jotai";
 | 
			
		||||
 | 
			
		||||
import { appLangCodeAtom } from "./language-state";
 | 
			
		||||
 | 
			
		||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import LanguageDetector from "i18next-browser-languagedetector";
 | 
			
		||||
import { defaultLang, languages } from "@excalidraw/excalidraw";
 | 
			
		||||
import LanguageDetector from "i18next-browser-languagedetector";
 | 
			
		||||
 | 
			
		||||
export const languageDetector = new LanguageDetector();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import { useEffect } from "react";
 | 
			
		||||
 | 
			
		||||
import { atom, useAtom } from "../app-jotai";
 | 
			
		||||
 | 
			
		||||
import { getPreferredLanguage, languageDetector } from "./language-detector";
 | 
			
		||||
 | 
			
		||||
export const appLangCodeAtom = atom(getPreferredLanguage());
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,3 @@
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
import { PureComponent } from "react";
 | 
			
		||||
import type {
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  ExcalidrawImperativeAPI,
 | 
			
		||||
  SocketId,
 | 
			
		||||
  Collaborator,
 | 
			
		||||
  Gesture,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
 | 
			
		||||
import { APP_NAME, ENV, EVENT } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
  InitializedExcalidrawImageElement,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import {
 | 
			
		||||
  CaptureUpdateAction,
 | 
			
		||||
  getSceneVersion,
 | 
			
		||||
@@ -23,12 +5,51 @@ import {
 | 
			
		||||
  zoomToFitBounds,
 | 
			
		||||
  reconcileElements,
 | 
			
		||||
} from "@excalidraw/excalidraw";
 | 
			
		||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
 | 
			
		||||
import { APP_NAME, EVENT } from "@excalidraw/common";
 | 
			
		||||
import {
 | 
			
		||||
  IDLE_THRESHOLD,
 | 
			
		||||
  ACTIVE_THRESHOLD,
 | 
			
		||||
  UserIdleState,
 | 
			
		||||
  assertNever,
 | 
			
		||||
  isDevEnv,
 | 
			
		||||
  isTestEnv,
 | 
			
		||||
  preventUnload,
 | 
			
		||||
  resolvablePromise,
 | 
			
		||||
  throttleRAF,
 | 
			
		||||
} from "@excalidraw/excalidraw/utils";
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import { getVisibleSceneBounds } from "@excalidraw/element";
 | 
			
		||||
import { newElementWith } from "@excalidraw/element";
 | 
			
		||||
import { isImageElement, isInitializedImageElement } from "@excalidraw/element";
 | 
			
		||||
import { AbortError } from "@excalidraw/excalidraw/errors";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
 | 
			
		||||
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
import { PureComponent } from "react";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ReconciledExcalidrawElement,
 | 
			
		||||
  RemoteExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/data/reconcile";
 | 
			
		||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
  InitializedExcalidrawImageElement,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  ExcalidrawImperativeAPI,
 | 
			
		||||
  SocketId,
 | 
			
		||||
  Collaborator,
 | 
			
		||||
  Gesture,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
 | 
			
		||||
 | 
			
		||||
import { appJotaiStore, atom } from "../app-jotai";
 | 
			
		||||
import {
 | 
			
		||||
  CURSOR_SYNC_TIMEOUT,
 | 
			
		||||
  FILE_UPLOAD_MAX_BYTES,
 | 
			
		||||
@@ -39,15 +60,17 @@ import {
 | 
			
		||||
  SYNC_FULL_SCENE_INTERVAL_MS,
 | 
			
		||||
  WS_EVENTS,
 | 
			
		||||
} from "../app_constants";
 | 
			
		||||
import type {
 | 
			
		||||
  SocketUpdateDataSource,
 | 
			
		||||
  SyncableExcalidrawElement,
 | 
			
		||||
} from "../data";
 | 
			
		||||
import {
 | 
			
		||||
  generateCollaborationLinkData,
 | 
			
		||||
  getCollaborationLink,
 | 
			
		||||
  getSyncableElements,
 | 
			
		||||
} from "../data";
 | 
			
		||||
import {
 | 
			
		||||
  encodeFilesForUpload,
 | 
			
		||||
  FileManager,
 | 
			
		||||
  updateStaleImageStatuses,
 | 
			
		||||
} from "../data/FileManager";
 | 
			
		||||
import { LocalData } from "../data/LocalData";
 | 
			
		||||
import {
 | 
			
		||||
  isSavedToFirebase,
 | 
			
		||||
  loadFilesFromFirebase,
 | 
			
		||||
@@ -59,36 +82,15 @@ import {
 | 
			
		||||
  importUsernameFromLocalStorage,
 | 
			
		||||
  saveUsernameToLocalStorage,
 | 
			
		||||
} from "../data/localStorage";
 | 
			
		||||
import Portal from "./Portal";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import {
 | 
			
		||||
  IDLE_THRESHOLD,
 | 
			
		||||
  ACTIVE_THRESHOLD,
 | 
			
		||||
  UserIdleState,
 | 
			
		||||
} from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import {
 | 
			
		||||
  encodeFilesForUpload,
 | 
			
		||||
  FileManager,
 | 
			
		||||
  updateStaleImageStatuses,
 | 
			
		||||
} from "../data/FileManager";
 | 
			
		||||
import { AbortError } from "@excalidraw/excalidraw/errors";
 | 
			
		||||
import {
 | 
			
		||||
  isImageElement,
 | 
			
		||||
  isInitializedImageElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/typeChecks";
 | 
			
		||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
 | 
			
		||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import { resetBrowserStateVersions } from "../data/tabSync";
 | 
			
		||||
import { LocalData } from "../data/LocalData";
 | 
			
		||||
import { appJotaiStore, atom } from "../app-jotai";
 | 
			
		||||
import type { Mutable, ValueOf } from "@excalidraw/excalidraw/utility-types";
 | 
			
		||||
import { getVisibleSceneBounds } from "@excalidraw/excalidraw/element/bounds";
 | 
			
		||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
 | 
			
		||||
 | 
			
		||||
import { collabErrorIndicatorAtom } from "./CollabError";
 | 
			
		||||
import Portal from "./Portal";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ReconciledExcalidrawElement,
 | 
			
		||||
  RemoteExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/data/reconcile";
 | 
			
		||||
  SocketUpdateDataSource,
 | 
			
		||||
  SyncableExcalidrawElement,
 | 
			
		||||
} from "../data";
 | 
			
		||||
 | 
			
		||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
 | 
			
		||||
export const isCollaboratingAtom = atom(false);
 | 
			
		||||
@@ -236,7 +238,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
			
		||||
 | 
			
		||||
    appJotaiStore.set(collabAPIAtom, collabAPI);
 | 
			
		||||
 | 
			
		||||
    if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
 | 
			
		||||
    if (isTestEnv() || isDevEnv()) {
 | 
			
		||||
      window.collab = window.collab || ({} as Window["collab"]);
 | 
			
		||||
      Object.defineProperties(window, {
 | 
			
		||||
        collab: {
 | 
			
		||||
@@ -296,7 +298,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
 | 
			
		||||
      //  the purpose is to run in immediately after user decides to stay
 | 
			
		||||
      this.saveCollabRoomToFirebase(syncableElements);
 | 
			
		||||
 | 
			
		||||
      preventUnload(event);
 | 
			
		||||
      if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
 | 
			
		||||
        preventUnload(event);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn(
 | 
			
		||||
          "preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -1009,7 +1017,7 @@ declare global {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
 | 
			
		||||
if (isTestEnv() || isDevEnv()) {
 | 
			
		||||
  window.collab = window.collab || ({} as Window["collab"]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
 | 
			
		||||
import { warning } from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import { atom } from "../app-jotai";
 | 
			
		||||
 | 
			
		||||
import "./CollabError.scss";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,26 @@
 | 
			
		||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
 | 
			
		||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
 | 
			
		||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import { newElementWith } from "@excalidraw/element";
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
 | 
			
		||||
import type { UserIdleState } from "@excalidraw/common";
 | 
			
		||||
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  OnUserFollowedPayload,
 | 
			
		||||
  SocketId,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
 | 
			
		||||
import { isSyncableElement } from "../data";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  SocketUpdateData,
 | 
			
		||||
  SocketUpdateDataSource,
 | 
			
		||||
  SyncableExcalidrawElement,
 | 
			
		||||
} from "../data";
 | 
			
		||||
import { isSyncableElement } from "../data";
 | 
			
		||||
 | 
			
		||||
import type { TCollabClass } from "./Collab";
 | 
			
		||||
 | 
			
		||||
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
 | 
			
		||||
import type {
 | 
			
		||||
  OnUserFollowedPayload,
 | 
			
		||||
  SocketId,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
 | 
			
		||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import type { Socket } from "socket.io-client";
 | 
			
		||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
 | 
			
		||||
 | 
			
		||||
class Portal {
 | 
			
		||||
  collab: TCollabClass;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
 | 
			
		||||
import {
 | 
			
		||||
  DiagramToCodePlugin,
 | 
			
		||||
  exportToBlob,
 | 
			
		||||
@@ -7,7 +6,9 @@ import {
 | 
			
		||||
  TTDDialog,
 | 
			
		||||
} from "@excalidraw/excalidraw";
 | 
			
		||||
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
 | 
			
		||||
import { safelyParseJSON } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import { safelyParseJSON } from "@excalidraw/common";
 | 
			
		||||
 | 
			
		||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
export const AIComponents = ({
 | 
			
		||||
  excalidrawAPI,
 | 
			
		||||
@@ -72,7 +73,7 @@ export const AIComponents = ({
 | 
			
		||||
                  </br>
 | 
			
		||||
                  <div>You can also try <a href="${
 | 
			
		||||
                    import.meta.env.VITE_APP_PLUS_LP
 | 
			
		||||
                  }/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
 | 
			
		||||
                  }/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
 | 
			
		||||
                </div>
 | 
			
		||||
                </body>
 | 
			
		||||
                </html>`,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { Footer } from "@excalidraw/excalidraw/index";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
 | 
			
		||||
 | 
			
		||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
 | 
			
		||||
import { EncryptedIcon } from "./EncryptedIcon";
 | 
			
		||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
 | 
			
		||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
 | 
			
		||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
 | 
			
		||||
 | 
			
		||||
export const AppFooter = React.memo(
 | 
			
		||||
  ({ onChange }: { onChange: () => void }) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,18 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import {
 | 
			
		||||
  loginIcon,
 | 
			
		||||
  ExcalLogo,
 | 
			
		||||
  eyeIcon,
 | 
			
		||||
} from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import { MainMenu } from "@excalidraw/excalidraw/index";
 | 
			
		||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { isDevEnv } from "@excalidraw/common";
 | 
			
		||||
 | 
			
		||||
import type { Theme } from "@excalidraw/element/types";
 | 
			
		||||
 | 
			
		||||
import { LanguageList } from "../app-language/LanguageList";
 | 
			
		||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
 | 
			
		||||
 | 
			
		||||
import { saveDebugState } from "./DebugCanvas";
 | 
			
		||||
 | 
			
		||||
export const AppMainMenu: React.FC<{
 | 
			
		||||
@@ -54,7 +59,7 @@ export const AppMainMenu: React.FC<{
 | 
			
		||||
      >
 | 
			
		||||
        {isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
 | 
			
		||||
      </MainMenu.ItemLink>
 | 
			
		||||
      {import.meta.env.DEV && (
 | 
			
		||||
      {isDevEnv() && (
 | 
			
		||||
        <MainMenu.Item
 | 
			
		||||
          icon={eyeIcon}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import { POINTER_EVENTS } from "@excalidraw/common";
 | 
			
		||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
 | 
			
		||||
import { POINTER_EVENTS } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
 | 
			
		||||
export const AppWelcomeScreen: React.FC<{
 | 
			
		||||
  onCollabDialogOpen: () => any;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,30 @@
 | 
			
		||||
import { useCallback, useImperativeHandle, useRef } from "react";
 | 
			
		||||
import { type AppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
import { throttleRAF } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import {
 | 
			
		||||
  bootstrapCanvas,
 | 
			
		||||
  getNormalizedCanvasDimensions,
 | 
			
		||||
} from "@excalidraw/excalidraw/renderer/helpers";
 | 
			
		||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
 | 
			
		||||
import {
 | 
			
		||||
  ArrowheadArrowIcon,
 | 
			
		||||
  CloseIcon,
 | 
			
		||||
  TrashIcon,
 | 
			
		||||
} from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import { STORAGE_KEYS } from "../app_constants";
 | 
			
		||||
import type { Curve } from "../../packages/math";
 | 
			
		||||
import {
 | 
			
		||||
  bootstrapCanvas,
 | 
			
		||||
  getNormalizedCanvasDimensions,
 | 
			
		||||
} from "@excalidraw/excalidraw/renderer/helpers";
 | 
			
		||||
import { type AppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
import { throttleRAF } from "@excalidraw/common";
 | 
			
		||||
import { useCallback } from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  isLineSegment,
 | 
			
		||||
  type GlobalPoint,
 | 
			
		||||
  type LineSegment,
 | 
			
		||||
} from "../../packages/math";
 | 
			
		||||
import { isCurve } from "../../packages/math/curve";
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
import { isCurve } from "@excalidraw/math/curve";
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import type { Curve } from "@excalidraw/math";
 | 
			
		||||
 | 
			
		||||
import type { DebugElement } from "@excalidraw/utils/visualdebug";
 | 
			
		||||
 | 
			
		||||
import { STORAGE_KEYS } from "../app_constants";
 | 
			
		||||
 | 
			
		||||
const renderLine = (
 | 
			
		||||
  context: CanvasRenderingContext2D,
 | 
			
		||||
@@ -109,10 +115,6 @@ const _debugRenderer = (
 | 
			
		||||
    scale,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (appState.height !== canvas.height || appState.width !== canvas.width) {
 | 
			
		||||
    refresh();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const context = bootstrapCanvas({
 | 
			
		||||
    canvas,
 | 
			
		||||
    scale,
 | 
			
		||||
@@ -310,35 +312,29 @@ export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
 | 
			
		||||
interface DebugCanvasProps {
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  scale: number;
 | 
			
		||||
  ref?: React.Ref<HTMLCanvasElement>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DebugCanvas = ({ appState, scale, ref }: DebugCanvasProps) => {
 | 
			
		||||
  const { width, height } = appState;
 | 
			
		||||
const DebugCanvas = React.forwardRef<HTMLCanvasElement, DebugCanvasProps>(
 | 
			
		||||
  ({ appState, scale }, ref) => {
 | 
			
		||||
    const { width, height } = appState;
 | 
			
		||||
 | 
			
		||||
  const canvasRef = useRef<HTMLCanvasElement>(null);
 | 
			
		||||
  useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
 | 
			
		||||
    ref,
 | 
			
		||||
    () => canvasRef.current,
 | 
			
		||||
    [canvasRef],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <canvas
 | 
			
		||||
      style={{
 | 
			
		||||
        width,
 | 
			
		||||
        height,
 | 
			
		||||
        position: "absolute",
 | 
			
		||||
        zIndex: 2,
 | 
			
		||||
        pointerEvents: "none",
 | 
			
		||||
      }}
 | 
			
		||||
      width={width * scale}
 | 
			
		||||
      height={height * scale}
 | 
			
		||||
      ref={canvasRef}
 | 
			
		||||
    >
 | 
			
		||||
      Debug Canvas
 | 
			
		||||
    </canvas>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
    return (
 | 
			
		||||
      <canvas
 | 
			
		||||
        style={{
 | 
			
		||||
          width,
 | 
			
		||||
          height,
 | 
			
		||||
          position: "absolute",
 | 
			
		||||
          zIndex: 2,
 | 
			
		||||
          pointerEvents: "none",
 | 
			
		||||
        }}
 | 
			
		||||
        width={width * scale}
 | 
			
		||||
        height={height * scale}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
      >
 | 
			
		||||
        Debug Canvas
 | 
			
		||||
      </canvas>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default DebugCanvas;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { shield } from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
 | 
			
		||||
import { shield } from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
 | 
			
		||||
export const EncryptedIcon = () => {
 | 
			
		||||
@@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
 | 
			
		||||
      className="encrypted-icon tooltip"
 | 
			
		||||
      href="https://plus.excalidraw.com/blog/end-to-end-encryption"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
      rel="noopener"
 | 
			
		||||
      aria-label={t("encrypted.link")}
 | 
			
		||||
    >
 | 
			
		||||
      <Tooltip label={t("encrypted.tooltip")} long={true}>
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
 | 
			
		||||
        import.meta.env.VITE_APP_PLUS_APP
 | 
			
		||||
      }?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noreferrer"
 | 
			
		||||
      rel="noopener"
 | 
			
		||||
      className="plus-button"
 | 
			
		||||
    >
 | 
			
		||||
      Go to Excalidraw+
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,33 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { uploadBytes, ref } from "firebase/storage";
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
 | 
			
		||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
 | 
			
		||||
import { Card } from "@excalidraw/excalidraw/components/Card";
 | 
			
		||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
 | 
			
		||||
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
 | 
			
		||||
import { MIME_TYPES, getFrame } from "@excalidraw/common";
 | 
			
		||||
import {
 | 
			
		||||
  encryptData,
 | 
			
		||||
  generateEncryptionKey,
 | 
			
		||||
} from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
 | 
			
		||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/element";
 | 
			
		||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  FileId,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  BinaryFiles,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import {
 | 
			
		||||
  encryptData,
 | 
			
		||||
  generateEncryptionKey,
 | 
			
		||||
} from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
 | 
			
		||||
 | 
			
		||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
 | 
			
		||||
import { encodeFilesForUpload } from "../data/FileManager";
 | 
			
		||||
import { uploadBytes, ref } from "firebase/storage";
 | 
			
		||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
 | 
			
		||||
import { getFrame } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
 | 
			
		||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
 | 
			
		||||
 | 
			
		||||
export const exportToExcalidrawPlus = async (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
import { THEME } from "@excalidraw/common";
 | 
			
		||||
import oc from "open-color";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { THEME } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
 | 
			
		||||
import type { Theme } from "@excalidraw/element/types";
 | 
			
		||||
 | 
			
		||||
// https://github.com/tholman/github-corners
 | 
			
		||||
export const GitHubCorner = React.memo(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import * as Sentry from "@sentry/browser";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import Trans from "@excalidraw/excalidraw/components/Trans";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import * as Sentry from "@sentry/browser";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
interface TopErrorBoundaryState {
 | 
			
		||||
  hasError: boolean;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
 | 
			
		||||
import { compressData } from "@excalidraw/excalidraw/data/encode";
 | 
			
		||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
 | 
			
		||||
import { newElementWith } from "@excalidraw/element";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/element";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawImageElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
  InitializedExcalidrawImageElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  BinaryFileMetadata,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,13 @@
 | 
			
		||||
 *   (localStorage, indexedDB).
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
 | 
			
		||||
import {
 | 
			
		||||
  CANVAS_SEARCH_TAB,
 | 
			
		||||
  DEFAULT_SIDEBAR,
 | 
			
		||||
  debounce,
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
 | 
			
		||||
import {
 | 
			
		||||
  createStore,
 | 
			
		||||
  entries,
 | 
			
		||||
@@ -19,26 +26,19 @@ import {
 | 
			
		||||
  setMany,
 | 
			
		||||
  get,
 | 
			
		||||
} from "idb-keyval";
 | 
			
		||||
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
 | 
			
		||||
import {
 | 
			
		||||
  CANVAS_SEARCH_TAB,
 | 
			
		||||
  DEFAULT_SIDEBAR,
 | 
			
		||||
} from "@excalidraw/excalidraw/constants";
 | 
			
		||||
 | 
			
		||||
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
 | 
			
		||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
 | 
			
		||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  BinaryFiles,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import type { MaybePromise } from "@excalidraw/excalidraw/utility-types";
 | 
			
		||||
import { debounce } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import type { MaybePromise } from "@excalidraw/common/utility-types";
 | 
			
		||||
 | 
			
		||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
 | 
			
		||||
 | 
			
		||||
import { FileManager } from "./FileManager";
 | 
			
		||||
import { Locker } from "./Locker";
 | 
			
		||||
import { updateBrowserStateVersion } from "./tabSync";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,12 @@
 | 
			
		||||
import { reconcileElements } from "@excalidraw/excalidraw";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import { getSceneVersion } from "@excalidraw/excalidraw/element";
 | 
			
		||||
import type Portal from "../collab/Portal";
 | 
			
		||||
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  BinaryFileMetadata,
 | 
			
		||||
  DataURL,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
 | 
			
		||||
import { MIME_TYPES } from "@excalidraw/common";
 | 
			
		||||
import { decompressData } from "@excalidraw/excalidraw/data/encode";
 | 
			
		||||
import {
 | 
			
		||||
  encryptData,
 | 
			
		||||
  decryptData,
 | 
			
		||||
} from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import type { SyncableExcalidrawElement } from ".";
 | 
			
		||||
import { getSyncableElements } from ".";
 | 
			
		||||
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
 | 
			
		||||
import { getSceneVersion } from "@excalidraw/element";
 | 
			
		||||
import { initializeApp } from "firebase/app";
 | 
			
		||||
import {
 | 
			
		||||
  getFirestore,
 | 
			
		||||
@@ -31,8 +16,27 @@ import {
 | 
			
		||||
  Bytes,
 | 
			
		||||
} from "firebase/firestore";
 | 
			
		||||
import { getStorage, ref, uploadBytes } from "firebase/storage";
 | 
			
		||||
import type { Socket } from "socket.io-client";
 | 
			
		||||
 | 
			
		||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  BinaryFileMetadata,
 | 
			
		||||
  DataURL,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
 | 
			
		||||
 | 
			
		||||
import { getSyncableElements } from ".";
 | 
			
		||||
 | 
			
		||||
import type { SyncableExcalidrawElement } from ".";
 | 
			
		||||
import type Portal from "../collab/Portal";
 | 
			
		||||
import type { Socket } from "socket.io-client";
 | 
			
		||||
 | 
			
		||||
// private
 | 
			
		||||
// -----------------------------------------------------------------------------
 | 
			
		||||
 
 | 
			
		||||
@@ -9,34 +9,38 @@ import {
 | 
			
		||||
} from "@excalidraw/excalidraw/data/encryption";
 | 
			
		||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
 | 
			
		||||
import { restore } from "@excalidraw/excalidraw/data/restore";
 | 
			
		||||
import { isInvisiblySmallElement } from "@excalidraw/element";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/element";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import { bytesToHexString } from "@excalidraw/common";
 | 
			
		||||
 | 
			
		||||
import type { UserIdleState } from "@excalidraw/common";
 | 
			
		||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
 | 
			
		||||
import type { SceneBounds } from "@excalidraw/excalidraw/element/bounds";
 | 
			
		||||
import { isInvisiblySmallElement } from "@excalidraw/excalidraw/element/sizeHelpers";
 | 
			
		||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
 | 
			
		||||
import type { SceneBounds } from "@excalidraw/element";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
} from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import { t } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  AppState,
 | 
			
		||||
  BinaryFileData,
 | 
			
		||||
  BinaryFiles,
 | 
			
		||||
  SocketId,
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import type { MakeBrand } from "@excalidraw/excalidraw/utility-types";
 | 
			
		||||
import { bytesToHexString } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import type { WS_SUBTYPES } from "../app_constants";
 | 
			
		||||
import type { MakeBrand } from "@excalidraw/common/utility-types";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  DELETED_ELEMENT_TIMEOUT,
 | 
			
		||||
  FILE_UPLOAD_MAX_BYTES,
 | 
			
		||||
  ROOM_ID_BYTES,
 | 
			
		||||
} from "../app_constants";
 | 
			
		||||
 | 
			
		||||
import { encodeFilesForUpload } from "./FileManager";
 | 
			
		||||
import { saveFilesToFirebase } from "./firebase";
 | 
			
		||||
 | 
			
		||||
import type { WS_SUBTYPES } from "../app_constants";
 | 
			
		||||
 | 
			
		||||
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
 | 
			
		||||
  MakeBrand<"SyncableExcalidrawElement">;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import type { AppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
import {
 | 
			
		||||
  clearAppStateForLocalStorage,
 | 
			
		||||
  getDefaultAppState,
 | 
			
		||||
} from "@excalidraw/excalidraw/appState";
 | 
			
		||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
 | 
			
		||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
 | 
			
		||||
 | 
			
		||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
 | 
			
		||||
import type { AppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { STORAGE_KEYS } from "../app_constants";
 | 
			
		||||
 | 
			
		||||
export const saveUsernameToLocalStorage = (username: string) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,9 @@
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <title>Excalidraw | Hand-drawn look & feel • Collaborative • Secure</title>
 | 
			
		||||
    <title>
 | 
			
		||||
      Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw
 | 
			
		||||
    </title>
 | 
			
		||||
    <meta
 | 
			
		||||
      name="viewport"
 | 
			
		||||
      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
 | 
			
		||||
@@ -14,7 +16,7 @@
 | 
			
		||||
    <!-- Primary Meta Tags -->
 | 
			
		||||
    <meta
 | 
			
		||||
      name="title"
 | 
			
		||||
      content="Excalidraw — Collaborative whiteboarding made easy"
 | 
			
		||||
      content="Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw"
 | 
			
		||||
    />
 | 
			
		||||
    <meta
 | 
			
		||||
      name="description"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
import { StrictMode } from "react";
 | 
			
		||||
import { createRoot } from "react-dom/client";
 | 
			
		||||
import ExcalidrawApp from "./App";
 | 
			
		||||
import { registerSW } from "virtual:pwa-register";
 | 
			
		||||
 | 
			
		||||
import "../excalidraw-app/sentry";
 | 
			
		||||
 | 
			
		||||
import ExcalidrawApp from "./App";
 | 
			
		||||
 | 
			
		||||
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
 | 
			
		||||
const rootElement = document.getElementById("root")!;
 | 
			
		||||
const root = createRoot(rootElement);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,8 @@
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
 | 
			
		||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
 | 
			
		||||
import { getFrame } from "@excalidraw/excalidraw/utils";
 | 
			
		||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import { KEYS } from "@excalidraw/excalidraw/keys";
 | 
			
		||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
 | 
			
		||||
import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
 | 
			
		||||
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
 | 
			
		||||
import { TextField } from "@excalidraw/excalidraw/components/TextField";
 | 
			
		||||
import {
 | 
			
		||||
  copyIcon,
 | 
			
		||||
  LinkIcon,
 | 
			
		||||
@@ -14,16 +12,19 @@ import {
 | 
			
		||||
  shareIOS,
 | 
			
		||||
  shareWindows,
 | 
			
		||||
} from "@excalidraw/excalidraw/components/icons";
 | 
			
		||||
import { TextField } from "@excalidraw/excalidraw/components/TextField";
 | 
			
		||||
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
 | 
			
		||||
import type { CollabAPI } from "../collab/Collab";
 | 
			
		||||
import { activeRoomLinkAtom } from "../collab/Collab";
 | 
			
		||||
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
 | 
			
		||||
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
 | 
			
		||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
 | 
			
		||||
import { KEYS, getFrame } from "@excalidraw/common";
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import { atom, useAtom, useAtomValue } from "../app-jotai";
 | 
			
		||||
import { activeRoomLinkAtom } from "../collab/Collab";
 | 
			
		||||
 | 
			
		||||
import "./ShareDialog.scss";
 | 
			
		||||
 | 
			
		||||
import type { CollabAPI } from "../collab/Collab";
 | 
			
		||||
 | 
			
		||||
type OnExportToBackend = () => void;
 | 
			
		||||
type ShareDialogType = "share" | "collaborationOnly";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import ExcalidrawApp from "../App";
 | 
			
		||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
 | 
			
		||||
import {
 | 
			
		||||
  mockBoundingClientRect,
 | 
			
		||||
  render,
 | 
			
		||||
  restoreOriginalGetBoundingClientRect,
 | 
			
		||||
} from "@excalidraw/excalidraw/tests/test-utils";
 | 
			
		||||
 | 
			
		||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
 | 
			
		||||
import ExcalidrawApp from "../App";
 | 
			
		||||
 | 
			
		||||
describe("Test MobileMenu", () => {
 | 
			
		||||
  const { h } = window;
 | 
			
		||||
 
 | 
			
		||||
@@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
 | 
			
		||||
    <a
 | 
			
		||||
      class="welcome-screen-menu-item "
 | 
			
		||||
      href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
 | 
			
		||||
      rel="noreferrer"
 | 
			
		||||
      rel="noopener"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,18 @@
 | 
			
		||||
import { vi } from "vitest";
 | 
			
		||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
 | 
			
		||||
import ExcalidrawApp from "../App";
 | 
			
		||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
 | 
			
		||||
import { syncInvalidIndices } from "@excalidraw/excalidraw/fractionalIndex";
 | 
			
		||||
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
 | 
			
		||||
import {
 | 
			
		||||
  createRedoAction,
 | 
			
		||||
  createUndoAction,
 | 
			
		||||
} from "@excalidraw/excalidraw/actions/actionHistory";
 | 
			
		||||
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
 | 
			
		||||
import { syncInvalidIndices } from "@excalidraw/element";
 | 
			
		||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
 | 
			
		||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
 | 
			
		||||
import { vi } from "vitest";
 | 
			
		||||
 | 
			
		||||
import { StoreIncrement } from "@excalidraw/element";
 | 
			
		||||
 | 
			
		||||
import type { DurableIncrement, EphemeralIncrement } from "@excalidraw/element";
 | 
			
		||||
 | 
			
		||||
import ExcalidrawApp from "../App";
 | 
			
		||||
 | 
			
		||||
const { h } = window;
 | 
			
		||||
 | 
			
		||||
@@ -64,6 +69,79 @@ vi.mock("socket.io-client", () => {
 | 
			
		||||
 * i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
 | 
			
		||||
 */
 | 
			
		||||
describe("collaboration", () => {
 | 
			
		||||
  it("should emit two ephemeral increments even though updates get batched", async () => {
 | 
			
		||||
    const durableIncrements: DurableIncrement[] = [];
 | 
			
		||||
    const ephemeralIncrements: EphemeralIncrement[] = [];
 | 
			
		||||
 | 
			
		||||
    await render(<ExcalidrawApp />);
 | 
			
		||||
 | 
			
		||||
    h.store.onStoreIncrementEmitter.on((increment) => {
 | 
			
		||||
      if (StoreIncrement.isDurable(increment)) {
 | 
			
		||||
        durableIncrements.push(increment);
 | 
			
		||||
      } else {
 | 
			
		||||
        ephemeralIncrements.push(increment);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line dot-notation
 | 
			
		||||
    expect(h.store["scheduledMicroActions"].length).toBe(0);
 | 
			
		||||
    expect(durableIncrements.length).toBe(0);
 | 
			
		||||
    expect(ephemeralIncrements.length).toBe(0);
 | 
			
		||||
 | 
			
		||||
    const rectProps = {
 | 
			
		||||
      type: "rectangle",
 | 
			
		||||
      id: "A",
 | 
			
		||||
      height: 200,
 | 
			
		||||
      width: 100,
 | 
			
		||||
      x: 0,
 | 
			
		||||
      y: 0,
 | 
			
		||||
    } as const;
 | 
			
		||||
 | 
			
		||||
    const rect = API.createElement({ ...rectProps });
 | 
			
		||||
 | 
			
		||||
    API.updateScene({
 | 
			
		||||
      elements: [rect],
 | 
			
		||||
      captureUpdate: CaptureUpdateAction.IMMEDIATELY,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      // expect(commitSpy).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(durableIncrements.length).toBe(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // simulate two batched remote updates
 | 
			
		||||
    act(() => {
 | 
			
		||||
      h.app.updateScene({
 | 
			
		||||
        elements: [newElementWith(h.elements[0], { x: 100 })],
 | 
			
		||||
        captureUpdate: CaptureUpdateAction.NEVER,
 | 
			
		||||
      });
 | 
			
		||||
      h.app.updateScene({
 | 
			
		||||
        elements: [newElementWith(h.elements[0], { x: 200 })],
 | 
			
		||||
        captureUpdate: CaptureUpdateAction.NEVER,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // we scheduled two micro actions,
 | 
			
		||||
      // which confirms they are going to be executed as part of one batched component update
 | 
			
		||||
      // eslint-disable-next-line dot-notation
 | 
			
		||||
      expect(h.store["scheduledMicroActions"].length).toBe(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      // altough the updates get batched,
 | 
			
		||||
      // we expect two ephemeral increments for each update,
 | 
			
		||||
      // and each such update should have the expected change
 | 
			
		||||
      expect(ephemeralIncrements.length).toBe(2);
 | 
			
		||||
      expect(ephemeralIncrements[0].change.elements.A).toEqual(
 | 
			
		||||
        expect.objectContaining({ x: 100 }),
 | 
			
		||||
      );
 | 
			
		||||
      expect(ephemeralIncrements[1].change.elements.A).toEqual(
 | 
			
		||||
        expect.objectContaining({ x: 200 }),
 | 
			
		||||
      );
 | 
			
		||||
      // eslint-disable-next-line dot-notation
 | 
			
		||||
      expect(h.store["scheduledMicroActions"].length).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("should allow to undo / redo even on force-deleted elements", async () => {
 | 
			
		||||
    await render(<ExcalidrawApp />);
 | 
			
		||||
    const rect1Props = {
 | 
			
		||||
@@ -121,12 +199,13 @@ describe("collaboration", () => {
 | 
			
		||||
      expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const undoAction = createUndoAction(h.history, h.store);
 | 
			
		||||
    const undoAction = createUndoAction(h.history);
 | 
			
		||||
    act(() => h.app.actionManager.executeAction(undoAction));
 | 
			
		||||
 | 
			
		||||
    // with explicit undo (as addition) we expect our item to be restored from the snapshot!
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(API.getUndoStack().length).toBe(1);
 | 
			
		||||
      expect(API.getRedoStack().length).toBe(1);
 | 
			
		||||
      expect(API.getSnapshot()).toEqual([
 | 
			
		||||
        expect.objectContaining(rect1Props),
 | 
			
		||||
        expect.objectContaining({ ...rect2Props, isDeleted: false }),
 | 
			
		||||
@@ -153,7 +232,7 @@ describe("collaboration", () => {
 | 
			
		||||
      expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const redoAction = createRedoAction(h.history, h.store);
 | 
			
		||||
    const redoAction = createRedoAction(h.history);
 | 
			
		||||
    act(() => h.app.actionManager.executeAction(redoAction));
 | 
			
		||||
 | 
			
		||||
    // with explicit redo (as removal) we again restore the element from the snapshot!
 | 
			
		||||
@@ -169,79 +248,5 @@ describe("collaboration", () => {
 | 
			
		||||
        expect.objectContaining({ ...rect2Props, isDeleted: true }),
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    act(() => h.app.actionManager.executeAction(undoAction));
 | 
			
		||||
 | 
			
		||||
    // simulate local update
 | 
			
		||||
    API.updateScene({
 | 
			
		||||
      elements: syncInvalidIndices([
 | 
			
		||||
        h.elements[0],
 | 
			
		||||
        newElementWith(h.elements[1], { x: 100 }),
 | 
			
		||||
      ]),
 | 
			
		||||
      captureUpdate: CaptureUpdateAction.IMMEDIATELY,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(API.getUndoStack().length).toBe(2);
 | 
			
		||||
      expect(API.getRedoStack().length).toBe(0);
 | 
			
		||||
      expect(API.getSnapshot()).toEqual([
 | 
			
		||||
        expect.objectContaining(rect1Props),
 | 
			
		||||
        expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(h.elements).toEqual([
 | 
			
		||||
        expect.objectContaining(rect1Props),
 | 
			
		||||
        expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    act(() => h.app.actionManager.executeAction(undoAction));
 | 
			
		||||
 | 
			
		||||
    // we expect to iterate the stack to the first visible change
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(API.getUndoStack().length).toBe(1);
 | 
			
		||||
      expect(API.getRedoStack().length).toBe(1);
 | 
			
		||||
      expect(API.getSnapshot()).toEqual([
 | 
			
		||||
        expect.objectContaining(rect1Props),
 | 
			
		||||
        expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(h.elements).toEqual([
 | 
			
		||||
        expect.objectContaining(rect1Props),
 | 
			
		||||
        expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // simulate force deleting the element remotely
 | 
			
		||||
    API.updateScene({
 | 
			
		||||
      elements: syncInvalidIndices([rect1]),
 | 
			
		||||
      captureUpdate: CaptureUpdateAction.NEVER,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // snapshot was correctly updated and marked the element as deleted
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(API.getUndoStack().length).toBe(1);
 | 
			
		||||
      expect(API.getRedoStack().length).toBe(1);
 | 
			
		||||
      expect(API.getSnapshot()).toEqual([
 | 
			
		||||
        expect.objectContaining(rect1Props),
 | 
			
		||||
        expect.objectContaining({ ...rect2Props, isDeleted: true, x: 0 }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    act(() => h.app.actionManager.executeAction(redoAction));
 | 
			
		||||
 | 
			
		||||
    // with explicit redo (as update) we again restored the element from the snapshot!
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(API.getUndoStack().length).toBe(2);
 | 
			
		||||
      expect(API.getRedoStack().length).toBe(0);
 | 
			
		||||
      expect(API.getSnapshot()).toEqual([
 | 
			
		||||
        expect.objectContaining({ id: "A", isDeleted: false }),
 | 
			
		||||
        expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(h.history.isRedoStackEmpty).toBeTruthy();
 | 
			
		||||
      expect(h.elements).toEqual([
 | 
			
		||||
        expect.objectContaining({ id: "A", isDeleted: false }),
 | 
			
		||||
        expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import { useEffect, useLayoutEffect, useState } from "react";
 | 
			
		||||
import { THEME } from "@excalidraw/excalidraw";
 | 
			
		||||
import { EVENT } from "@excalidraw/excalidraw/constants";
 | 
			
		||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
 | 
			
		||||
import { CODES, KEYS } from "@excalidraw/excalidraw/keys";
 | 
			
		||||
import { EVENT, CODES, KEYS } from "@excalidraw/common";
 | 
			
		||||
import { useEffect, useLayoutEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import type { Theme } from "@excalidraw/element/types";
 | 
			
		||||
 | 
			
		||||
import { STORAGE_KEYS } from "./app_constants";
 | 
			
		||||
 | 
			
		||||
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,29 +23,57 @@ export default defineConfig(({ mode }) => {
 | 
			
		||||
    envDir: "../",
 | 
			
		||||
    resolve: {
 | 
			
		||||
      alias: [
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/common$/,
 | 
			
		||||
          replacement: path.resolve(
 | 
			
		||||
            __dirname,
 | 
			
		||||
            "../packages/common/src/index.ts",
 | 
			
		||||
          ),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/common\/(.*?)/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/common/src/$1"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/element$/,
 | 
			
		||||
          replacement: path.resolve(
 | 
			
		||||
            __dirname,
 | 
			
		||||
            "../packages/element/src/index.ts",
 | 
			
		||||
          ),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/element\/(.*?)/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/element/src/$1"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/excalidraw$/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
 | 
			
		||||
          replacement: path.resolve(
 | 
			
		||||
            __dirname,
 | 
			
		||||
            "../packages/excalidraw/index.tsx",
 | 
			
		||||
          ),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/excalidraw\/(.*?)/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/utils$/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/utils/index.ts"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/utils\/(.*?)/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/utils/$1"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/math$/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/math/index.ts"),
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/math/src/index.ts"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/math\/(.*?)/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/math/$1"),
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/math/src/$1"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/utils$/,
 | 
			
		||||
          replacement: path.resolve(
 | 
			
		||||
            __dirname,
 | 
			
		||||
            "../packages/utils/src/index.ts",
 | 
			
		||||
          ),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          find: /^@excalidraw\/utils\/(.*?)/,
 | 
			
		||||
          replacement: path.resolve(__dirname, "../packages/utils/src/$1"),
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
@@ -197,7 +225,7 @@ export default defineConfig(({ mode }) => {
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          start_url: "/",
 | 
			
		||||
          id:"excalidraw",
 | 
			
		||||
          id: "excalidraw",
 | 
			
		||||
          display: "standalone",
 | 
			
		||||
          theme_color: "#121212",
 | 
			
		||||
          background_color: "#ffffff",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								package.json
									
									
									
									
									
								
							@@ -4,9 +4,7 @@
 | 
			
		||||
  "packageManager": "yarn@1.22.22",
 | 
			
		||||
  "workspaces": [
 | 
			
		||||
    "excalidraw-app",
 | 
			
		||||
    "packages/excalidraw",
 | 
			
		||||
    "packages/utils",
 | 
			
		||||
    "packages/math",
 | 
			
		||||
    "packages/*",
 | 
			
		||||
    "examples/*"
 | 
			
		||||
  ],
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
@@ -26,6 +24,7 @@
 | 
			
		||||
    "dotenv": "16.0.1",
 | 
			
		||||
    "eslint-config-prettier": "8.5.0",
 | 
			
		||||
    "eslint-config-react-app": "7.0.1",
 | 
			
		||||
    "eslint-plugin-import": "2.31.0",
 | 
			
		||||
    "eslint-plugin-prettier": "3.3.1",
 | 
			
		||||
    "http-server": "14.1.1",
 | 
			
		||||
    "husky": "7.0.4",
 | 
			
		||||
@@ -34,6 +33,7 @@
 | 
			
		||||
    "pepjs": "0.5.3",
 | 
			
		||||
    "prettier": "2.6.2",
 | 
			
		||||
    "rewire": "6.0.0",
 | 
			
		||||
    "rimraf": "^5.0.0",
 | 
			
		||||
    "typescript": "4.9.4",
 | 
			
		||||
    "vite": "5.0.12",
 | 
			
		||||
    "vite-plugin-checker": "0.7.2",
 | 
			
		||||
@@ -52,13 +52,17 @@
 | 
			
		||||
    "build-node": "node ./scripts/build-node.js",
 | 
			
		||||
    "build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
 | 
			
		||||
    "build:app": "yarn --cwd ./excalidraw-app build:app",
 | 
			
		||||
    "build:package": "yarn --cwd ./packages/excalidraw build:esm",
 | 
			
		||||
    "build:common": "yarn --cwd ./packages/common build:esm",
 | 
			
		||||
    "build:element": "yarn --cwd ./packages/element build:esm",
 | 
			
		||||
    "build:excalidraw": "yarn --cwd ./packages/excalidraw build:esm",
 | 
			
		||||
    "build:math": "yarn --cwd ./packages/math build:esm",
 | 
			
		||||
    "build:packages": "yarn build:common && yarn build:math && yarn build:element && yarn build:excalidraw",
 | 
			
		||||
    "build:version": "yarn --cwd ./excalidraw-app build:version",
 | 
			
		||||
    "build": "yarn --cwd ./excalidraw-app build",
 | 
			
		||||
    "build:preview": "yarn --cwd ./excalidraw-app build:preview",
 | 
			
		||||
    "start": "yarn --cwd ./excalidraw-app start",
 | 
			
		||||
    "start:production": "yarn --cwd ./excalidraw-app start:production",
 | 
			
		||||
    "start:example": "yarn build:package && yarn --cwd ./examples/with-script-in-browser start",
 | 
			
		||||
    "start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
 | 
			
		||||
    "test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
 | 
			
		||||
    "test:app": "vitest",
 | 
			
		||||
    "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
 | 
			
		||||
@@ -76,11 +80,12 @@
 | 
			
		||||
    "locales-coverage:description": "node scripts/locales-coverage-description.js",
 | 
			
		||||
    "prepare": "husky install",
 | 
			
		||||
    "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
 | 
			
		||||
    "autorelease": "node scripts/autorelease.js",
 | 
			
		||||
    "prerelease:excalidraw": "node scripts/prerelease.js",
 | 
			
		||||
    "release:excalidraw": "node scripts/release.js",
 | 
			
		||||
    "rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/{build,dist}",
 | 
			
		||||
    "rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
 | 
			
		||||
    "release": "node scripts/release.js",
 | 
			
		||||
    "release:test": "node scripts/release.js --tag=test",
 | 
			
		||||
    "release:next": "node scripts/release.js --tag=next",
 | 
			
		||||
    "release:latest": "node scripts/release.js --tag=latest",
 | 
			
		||||
    "rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist",
 | 
			
		||||
    "rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
 | 
			
		||||
    "clean-install": "yarn rm:node_modules && yarn install"
 | 
			
		||||
  },
 | 
			
		||||
  "resolutions": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/common/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/common/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": ["../eslintrc.base.json"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								packages/common/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/common/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# @excalidraw/common
 | 
			
		||||
 | 
			
		||||
## Install
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm install @excalidraw/common
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
yarn add @excalidraw/common
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With PNPM, similarly install the package with this command:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
pnpm add @excalidraw/common
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/common/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/common/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
/// <reference types="vite/client" />
 | 
			
		||||
import "@excalidraw/excalidraw/global";
 | 
			
		||||
import "@excalidraw/excalidraw/css";
 | 
			
		||||
							
								
								
									
										59
									
								
								packages/common/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								packages/common/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@excalidraw/common",
 | 
			
		||||
  "version": "0.18.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "types": "./dist/types/common/src/index.d.ts",
 | 
			
		||||
  "main": "./dist/prod/index.js",
 | 
			
		||||
  "module": "./dist/prod/index.js",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": {
 | 
			
		||||
      "types": "./dist/types/common/src/index.d.ts",
 | 
			
		||||
      "development": "./dist/dev/index.js",
 | 
			
		||||
      "production": "./dist/prod/index.js",
 | 
			
		||||
      "default": "./dist/prod/index.js"
 | 
			
		||||
    },
 | 
			
		||||
    "./*": {
 | 
			
		||||
      "types": "./dist/types/common/src/*.d.ts",
 | 
			
		||||
      "development": "./dist/dev/index.js",
 | 
			
		||||
      "production": "./dist/prod/index.js",
 | 
			
		||||
      "default": "./dist/prod/index.js"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "files": [
 | 
			
		||||
    "dist/*"
 | 
			
		||||
  ],
 | 
			
		||||
  "description": "Excalidraw common functions, constants, etc.",
 | 
			
		||||
  "publishConfig": {
 | 
			
		||||
    "access": "public"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "excalidraw",
 | 
			
		||||
    "excalidraw-utils"
 | 
			
		||||
  ],
 | 
			
		||||
  "browserslist": {
 | 
			
		||||
    "production": [
 | 
			
		||||
      ">0.2%",
 | 
			
		||||
      "not dead",
 | 
			
		||||
      "not ie <= 11",
 | 
			
		||||
      "not op_mini all",
 | 
			
		||||
      "not safari < 12",
 | 
			
		||||
      "not kaios <= 2.5",
 | 
			
		||||
      "not edge < 79",
 | 
			
		||||
      "not chrome < 70",
 | 
			
		||||
      "not and_uc < 13",
 | 
			
		||||
      "not samsung < 10"
 | 
			
		||||
    ],
 | 
			
		||||
    "development": [
 | 
			
		||||
      "last 1 chrome version",
 | 
			
		||||
      "last 1 firefox version",
 | 
			
		||||
      "last 1 safari version"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "bugs": "https://github.com/excalidraw/excalidraw/issues",
 | 
			
		||||
  "repository": "https://github.com/excalidraw/excalidraw",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "gen:types": "rimraf types && tsc",
 | 
			
		||||
    "build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export default class BinaryHeap<T> {
 | 
			
		||||
export class BinaryHeap<T> {
 | 
			
		||||
  private content: T[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor(private scoreFunction: (node: T) => number) {}
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
import oc from "open-color";
 | 
			
		||||
 | 
			
		||||
import type { Merge } from "./utility-types";
 | 
			
		||||
 | 
			
		||||
export const COLOR_OUTLINE_CONTRAST_THRESHOLD = 240;
 | 
			
		||||
 | 
			
		||||
// FIXME can't put to utils.ts rn because of circular dependency
 | 
			
		||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
 | 
			
		||||
  source: R,
 | 
			
		||||
@@ -1,11 +1,16 @@
 | 
			
		||||
import type { AppProps, AppState } from "./types";
 | 
			
		||||
import type { ExcalidrawElement, FontFamilyValues } from "./element/types";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { COLOR_PALETTE } from "./colors";
 | 
			
		||||
 | 
			
		||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
 | 
			
		||||
export const isWindows = /^Win/.test(navigator.platform);
 | 
			
		||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
 | 
			
		||||
export const isFirefox =
 | 
			
		||||
  typeof window !== "undefined" &&
 | 
			
		||||
  "netscape" in window &&
 | 
			
		||||
  navigator.userAgent.indexOf("rv:") > 1 &&
 | 
			
		||||
  navigator.userAgent.indexOf("Gecko") > 1;
 | 
			
		||||
@@ -31,6 +36,7 @@ export const APP_NAME = "Excalidraw";
 | 
			
		||||
// (happens a lot with fast clicks with the text tool)
 | 
			
		||||
export const TEXT_AUTOWRAP_THRESHOLD = 36; // px
 | 
			
		||||
export const DRAGGING_THRESHOLD = 10; // px
 | 
			
		||||
export const MINIMUM_ARROW_SIZE = 20; // px
 | 
			
		||||
export const LINE_CONFIRM_THRESHOLD = 8; // px
 | 
			
		||||
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
 | 
			
		||||
export const ELEMENT_TRANSLATE_AMOUNT = 1;
 | 
			
		||||
@@ -108,12 +114,14 @@ export const YOUTUBE_STATES = {
 | 
			
		||||
export const ENV = {
 | 
			
		||||
  TEST: "test",
 | 
			
		||||
  DEVELOPMENT: "development",
 | 
			
		||||
  PRODUCTION: "production",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const CLASSES = {
 | 
			
		||||
  SHAPE_ACTIONS_MENU: "App-menu__left",
 | 
			
		||||
  ZOOM_ACTIONS: "zoom-actions",
 | 
			
		||||
  SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
 | 
			
		||||
  CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
 | 
			
		||||
@@ -137,21 +145,52 @@ export const FONT_FAMILY = {
 | 
			
		||||
  "Lilita One": 7,
 | 
			
		||||
  "Comic Shanns": 8,
 | 
			
		||||
  "Liberation Sans": 9,
 | 
			
		||||
  Assistant: 10,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Segoe UI Emoji fails to properly fallback for some glyphs: ∞, ∫, ≠
 | 
			
		||||
// so we need to have generic font fallback before it
 | 
			
		||||
export const SANS_SERIF_GENERIC_FONT = "sans-serif";
 | 
			
		||||
export const MONOSPACE_GENERIC_FONT = "monospace";
 | 
			
		||||
 | 
			
		||||
export const FONT_FAMILY_GENERIC_FALLBACKS = {
 | 
			
		||||
  [SANS_SERIF_GENERIC_FONT]: 998,
 | 
			
		||||
  [MONOSPACE_GENERIC_FONT]: 999,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const FONT_FAMILY_FALLBACKS = {
 | 
			
		||||
  [CJK_HAND_DRAWN_FALLBACK_FONT]: 100,
 | 
			
		||||
  ...FONT_FAMILY_GENERIC_FALLBACKS,
 | 
			
		||||
  [WINDOWS_EMOJI_FALLBACK_FONT]: 1000,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getGenericFontFamilyFallback(
 | 
			
		||||
  fontFamily: number,
 | 
			
		||||
): keyof typeof FONT_FAMILY_GENERIC_FALLBACKS {
 | 
			
		||||
  switch (fontFamily) {
 | 
			
		||||
    case FONT_FAMILY.Cascadia:
 | 
			
		||||
    case FONT_FAMILY["Comic Shanns"]:
 | 
			
		||||
      return MONOSPACE_GENERIC_FONT;
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      return SANS_SERIF_GENERIC_FONT;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getFontFamilyFallbacks = (
 | 
			
		||||
  fontFamily: number,
 | 
			
		||||
): Array<keyof typeof FONT_FAMILY_FALLBACKS> => {
 | 
			
		||||
  const genericFallbackFont = getGenericFontFamilyFallback(fontFamily);
 | 
			
		||||
 | 
			
		||||
  switch (fontFamily) {
 | 
			
		||||
    case FONT_FAMILY.Excalifont:
 | 
			
		||||
      return [CJK_HAND_DRAWN_FALLBACK_FONT, WINDOWS_EMOJI_FALLBACK_FONT];
 | 
			
		||||
      return [
 | 
			
		||||
        CJK_HAND_DRAWN_FALLBACK_FONT,
 | 
			
		||||
        genericFallbackFont,
 | 
			
		||||
        WINDOWS_EMOJI_FALLBACK_FONT,
 | 
			
		||||
      ];
 | 
			
		||||
    default:
 | 
			
		||||
      return [WINDOWS_EMOJI_FALLBACK_FONT];
 | 
			
		||||
      return [genericFallbackFont, WINDOWS_EMOJI_FALLBACK_FONT];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -248,7 +287,7 @@ export const EXPORT_DATA_TYPES = {
 | 
			
		||||
  excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const EXPORT_SOURCE =
 | 
			
		||||
export const getExportSource = () =>
 | 
			
		||||
  window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
 | 
			
		||||
 | 
			
		||||
// time in milliseconds
 | 
			
		||||
@@ -314,6 +353,9 @@ export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
 | 
			
		||||
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
 | 
			
		||||
 | 
			
		||||
export const SVG_NS = "http://www.w3.org/2000/svg";
 | 
			
		||||
export const SVG_DOCUMENT_PREAMBLE = `<?xml version="1.0" standalone="no"?>
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ENCRYPTION_KEY_BITS = 128;
 | 
			
		||||
 | 
			
		||||
@@ -415,6 +457,7 @@ export const LIBRARY_DISABLED_TYPES = new Set([
 | 
			
		||||
// use these constants to easily identify reference sites
 | 
			
		||||
export const TOOL_TYPE = {
 | 
			
		||||
  selection: "selection",
 | 
			
		||||
  lasso: "lasso",
 | 
			
		||||
  rectangle: "rectangle",
 | 
			
		||||
  diamond: "diamond",
 | 
			
		||||
  ellipse: "ellipse",
 | 
			
		||||
@@ -465,3 +508,10 @@ export enum UserIdleState {
 | 
			
		||||
  AWAY = "away",
 | 
			
		||||
  IDLE = "idle",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * distance at which we merge points instead of adding a new merge-point
 | 
			
		||||
 * when converting a line to a polygon (merge currently means overlaping
 | 
			
		||||
 * the start and end points)
 | 
			
		||||
 */
 | 
			
		||||
export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import type { UnsubscribeCallback } from "./types";
 | 
			
		||||
import type { UnsubscribeCallback } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
type Subscriber<T extends any[]> = (...payload: T) => void;
 | 
			
		||||
 | 
			
		||||
@@ -1,11 +1,9 @@
 | 
			
		||||
import type { JSX } from "react";
 | 
			
		||||
import {
 | 
			
		||||
  FreedrawIcon,
 | 
			
		||||
  FontFamilyNormalIcon,
 | 
			
		||||
  FontFamilyHeadingIcon,
 | 
			
		||||
  FontFamilyCodeIcon,
 | 
			
		||||
} from "../components/icons";
 | 
			
		||||
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "../constants";
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
 | 
			
		||||
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "./constants";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Encapsulates font metrics with additional font metadata.
 | 
			
		||||
@@ -22,12 +20,12 @@ export interface FontMetadata {
 | 
			
		||||
    /** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
 | 
			
		||||
    lineHeight: number;
 | 
			
		||||
  };
 | 
			
		||||
  /** element to be displayed as an icon  */
 | 
			
		||||
  icon?: JSX.Element;
 | 
			
		||||
  /** flag to indicate a deprecated font */
 | 
			
		||||
  deprecated?: true;
 | 
			
		||||
  /** flag to indicate a server-side only font */
 | 
			
		||||
  serverSide?: true;
 | 
			
		||||
  /**
 | 
			
		||||
   * whether this is a font that users can use (= shown in font picker)
 | 
			
		||||
   */
 | 
			
		||||
  private?: true;
 | 
			
		||||
  /** flag to indiccate a local-only font */
 | 
			
		||||
  local?: true;
 | 
			
		||||
  /** flag to indicate a fallback font */
 | 
			
		||||
@@ -42,16 +40,14 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
      descender: -374,
 | 
			
		||||
      lineHeight: 1.25,
 | 
			
		||||
    },
 | 
			
		||||
    icon: FreedrawIcon,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY.Nunito]: {
 | 
			
		||||
    metrics: {
 | 
			
		||||
      unitsPerEm: 1000,
 | 
			
		||||
      ascender: 1011,
 | 
			
		||||
      descender: -353,
 | 
			
		||||
      lineHeight: 1.35,
 | 
			
		||||
      lineHeight: 1.25,
 | 
			
		||||
    },
 | 
			
		||||
    icon: FontFamilyNormalIcon,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY["Lilita One"]]: {
 | 
			
		||||
    metrics: {
 | 
			
		||||
@@ -60,7 +56,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
      descender: -220,
 | 
			
		||||
      lineHeight: 1.15,
 | 
			
		||||
    },
 | 
			
		||||
    icon: FontFamilyHeadingIcon,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY["Comic Shanns"]]: {
 | 
			
		||||
    metrics: {
 | 
			
		||||
@@ -69,7 +64,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
      descender: -250,
 | 
			
		||||
      lineHeight: 1.25,
 | 
			
		||||
    },
 | 
			
		||||
    icon: FontFamilyCodeIcon,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY.Virgil]: {
 | 
			
		||||
    metrics: {
 | 
			
		||||
@@ -78,7 +72,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
      descender: -374,
 | 
			
		||||
      lineHeight: 1.25,
 | 
			
		||||
    },
 | 
			
		||||
    icon: FreedrawIcon,
 | 
			
		||||
    deprecated: true,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY.Helvetica]: {
 | 
			
		||||
@@ -88,7 +81,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
      descender: -471,
 | 
			
		||||
      lineHeight: 1.15,
 | 
			
		||||
    },
 | 
			
		||||
    icon: FontFamilyNormalIcon,
 | 
			
		||||
    deprecated: true,
 | 
			
		||||
    local: true,
 | 
			
		||||
  },
 | 
			
		||||
@@ -99,7 +91,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
      descender: -480,
 | 
			
		||||
      lineHeight: 1.2,
 | 
			
		||||
    },
 | 
			
		||||
    icon: FontFamilyCodeIcon,
 | 
			
		||||
    deprecated: true,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY["Liberation Sans"]]: {
 | 
			
		||||
@@ -109,14 +100,23 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
      descender: -434,
 | 
			
		||||
      lineHeight: 1.15,
 | 
			
		||||
    },
 | 
			
		||||
    serverSide: true,
 | 
			
		||||
    private: true,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY.Assistant]: {
 | 
			
		||||
    metrics: {
 | 
			
		||||
      unitsPerEm: 2048,
 | 
			
		||||
      ascender: 1021,
 | 
			
		||||
      descender: -287,
 | 
			
		||||
      lineHeight: 1.25,
 | 
			
		||||
    },
 | 
			
		||||
    private: true,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY_FALLBACKS.Xiaolai]: {
 | 
			
		||||
    metrics: {
 | 
			
		||||
      unitsPerEm: 1000,
 | 
			
		||||
      ascender: 880,
 | 
			
		||||
      descender: -144,
 | 
			
		||||
      lineHeight: 1.15,
 | 
			
		||||
      lineHeight: 1.25,
 | 
			
		||||
    },
 | 
			
		||||
    fallback: true,
 | 
			
		||||
  },
 | 
			
		||||
@@ -148,3 +148,34 @@ export const GOOGLE_FONTS_RANGES = {
 | 
			
		||||
 | 
			
		||||
/** local protocol to skip the local font from registering or inlining */
 | 
			
		||||
export const LOCAL_FONT_PROTOCOL = "local:";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculates vertical offset for a text with alphabetic baseline.
 | 
			
		||||
 */
 | 
			
		||||
export const getVerticalOffset = (
 | 
			
		||||
  fontFamily: ExcalidrawTextElement["fontFamily"],
 | 
			
		||||
  fontSize: ExcalidrawTextElement["fontSize"],
 | 
			
		||||
  lineHeightPx: number,
 | 
			
		||||
) => {
 | 
			
		||||
  const { unitsPerEm, ascender, descender } =
 | 
			
		||||
    FONT_METADATA[fontFamily]?.metrics ||
 | 
			
		||||
    FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
 | 
			
		||||
 | 
			
		||||
  const fontSizeEm = fontSize / unitsPerEm;
 | 
			
		||||
  const lineGap =
 | 
			
		||||
    (lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender) / 2;
 | 
			
		||||
 | 
			
		||||
  const verticalOffset = fontSizeEm * ascender + lineGap;
 | 
			
		||||
  return verticalOffset;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets line height for a selected family.
 | 
			
		||||
 */
 | 
			
		||||
export const getLineHeight = (fontFamily: FontFamilyValues) => {
 | 
			
		||||
  const { lineHeight } =
 | 
			
		||||
    FONT_METADATA[fontFamily]?.metrics ||
 | 
			
		||||
    FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
 | 
			
		||||
 | 
			
		||||
  return lineHeight as ExcalidrawTextElement["lineHeight"];
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										12
									
								
								packages/common/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/common/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
export * from "./binary-heap";
 | 
			
		||||
export * from "./colors";
 | 
			
		||||
export * from "./constants";
 | 
			
		||||
export * from "./font-metadata";
 | 
			
		||||
export * from "./queue";
 | 
			
		||||
export * from "./keys";
 | 
			
		||||
export * from "./points";
 | 
			
		||||
export * from "./promise-pool";
 | 
			
		||||
export * from "./random";
 | 
			
		||||
export * from "./url";
 | 
			
		||||
export * from "./utils";
 | 
			
		||||
export * from "./emitter";
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { isDarwin } from "./constants";
 | 
			
		||||
 | 
			
		||||
import type { ValueOf } from "./utility-types";
 | 
			
		||||
 | 
			
		||||
export const CODES = {
 | 
			
		||||
@@ -4,6 +4,8 @@ import {
 | 
			
		||||
  type LocalPoint,
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
 | 
			
		||||
import type { NullableGridSize } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
export const getSizeFromPoints = (
 | 
			
		||||
  points: readonly (GlobalPoint | LocalPoint)[],
 | 
			
		||||
) => {
 | 
			
		||||
@@ -61,3 +63,18 @@ export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
 | 
			
		||||
 | 
			
		||||
  return nextPoints;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// TODO: Rounding this point causes some shake when free drawing
 | 
			
		||||
export const getGridPoint = (
 | 
			
		||||
  x: number,
 | 
			
		||||
  y: number,
 | 
			
		||||
  gridSize: NullableGridSize,
 | 
			
		||||
): [number, number] => {
 | 
			
		||||
  if (gridSize) {
 | 
			
		||||
    return [
 | 
			
		||||
      Math.round(x / gridSize) * gridSize,
 | 
			
		||||
      Math.round(y / gridSize) * gridSize,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  return [x, y];
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										50
									
								
								packages/common/src/promise-pool.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								packages/common/src/promise-pool.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import Pool from "es6-promise-pool";
 | 
			
		||||
 | 
			
		||||
// extending the missing types
 | 
			
		||||
// relying on the [Index, T] to keep a correct order
 | 
			
		||||
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
 | 
			
		||||
  addEventListener: (
 | 
			
		||||
    type: "fulfilled",
 | 
			
		||||
    listener: (event: { data: { result: [Index, T] } }) => void,
 | 
			
		||||
  ) => (event: { data: { result: [Index, T] } }) => void;
 | 
			
		||||
  removeEventListener: (
 | 
			
		||||
    type: "fulfilled",
 | 
			
		||||
    listener: (event: { data: { result: [Index, T] } }) => void,
 | 
			
		||||
  ) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class PromisePool<T> {
 | 
			
		||||
  private readonly pool: TPromisePool<T>;
 | 
			
		||||
  private readonly entries: Record<number, T> = {};
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    source: IterableIterator<Promise<void | readonly [number, T]>>,
 | 
			
		||||
    concurrency: number,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.pool = new Pool(
 | 
			
		||||
      source as unknown as () => void | PromiseLike<[number, T][]>,
 | 
			
		||||
      concurrency,
 | 
			
		||||
    ) as TPromisePool<T>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public all() {
 | 
			
		||||
    const listener = (event: { data: { result: void | [number, T] } }) => {
 | 
			
		||||
      if (event.data.result) {
 | 
			
		||||
        // by default pool does not return the results, so we are gathering them manually
 | 
			
		||||
        // with the correct call order (represented by the index in the tuple)
 | 
			
		||||
        const [index, value] = event.data.result;
 | 
			
		||||
        this.entries[index] = value;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.pool.addEventListener("fulfilled", listener);
 | 
			
		||||
 | 
			
		||||
    return this.pool.start().then(() => {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.pool.removeEventListener("fulfilled", listener);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return Object.values(this.entries);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
import { promiseTry, resolvablePromise } from ".";
 | 
			
		||||
 | 
			
		||||
import type { ResolvablePromise } from ".";
 | 
			
		||||
 | 
			
		||||
import type { MaybePromise } from "./utility-types";
 | 
			
		||||
import type { ResolvablePromise } from "./utils";
 | 
			
		||||
import { promiseTry, resolvablePromise } from "./utils";
 | 
			
		||||
 | 
			
		||||
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;
 | 
			
		||||
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { Random } from "roughjs/bin/math";
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
import { Random } from "roughjs/bin/math";
 | 
			
		||||
 | 
			
		||||
import { isTestEnv } from "./utils";
 | 
			
		||||
 | 
			
		||||
let random = new Random(Date.now());
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { sanitizeUrl } from "@braintree/sanitize-url";
 | 
			
		||||
import { escapeDoubleQuotes } from "../utils";
 | 
			
		||||
 | 
			
		||||
import { escapeDoubleQuotes } from "./utils";
 | 
			
		||||
 | 
			
		||||
export const normalizeLink = (link: string) => {
 | 
			
		||||
  link = link.trim();
 | 
			
		||||
@@ -68,3 +68,12 @@ export type MaybePromise<T> = T | Promise<T>;
 | 
			
		||||
 | 
			
		||||
// get union of all keys from the union of types
 | 
			
		||||
export type AllPossibleKeys<T> = T extends any ? keyof T : never;
 | 
			
		||||
 | 
			
		||||
/** Strip all the methods or functions from a type */
 | 
			
		||||
export type DTO<T> = {
 | 
			
		||||
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type MapEntry<M extends Map<any, any>> = M extends Map<infer K, infer V>
 | 
			
		||||
  ? [K, V]
 | 
			
		||||
  : never;
 | 
			
		||||
							
								
								
									
										82
									
								
								packages/common/src/utils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								packages/common/src/utils.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import {
 | 
			
		||||
  isTransparent,
 | 
			
		||||
  mapFind,
 | 
			
		||||
  reduceToCommonValue,
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
 | 
			
		||||
describe("@excalidraw/common/utils", () => {
 | 
			
		||||
  describe("isTransparent()", () => {
 | 
			
		||||
    it("should return true when color is rgb transparent", () => {
 | 
			
		||||
      expect(isTransparent("#ff00")).toEqual(true);
 | 
			
		||||
      expect(isTransparent("#fff00000")).toEqual(true);
 | 
			
		||||
      expect(isTransparent("transparent")).toEqual(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return false when color is not transparent", () => {
 | 
			
		||||
      expect(isTransparent("#ced4da")).toEqual(false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("reduceToCommonValue()", () => {
 | 
			
		||||
    it("should return the common value when all values are the same", () => {
 | 
			
		||||
      expect(reduceToCommonValue([1, 1])).toEqual(1);
 | 
			
		||||
      expect(reduceToCommonValue([0, 0])).toEqual(0);
 | 
			
		||||
      expect(reduceToCommonValue(["a", "a"])).toEqual("a");
 | 
			
		||||
      expect(reduceToCommonValue(new Set([1]))).toEqual(1);
 | 
			
		||||
      expect(reduceToCommonValue([""])).toEqual("");
 | 
			
		||||
      expect(reduceToCommonValue([0])).toEqual(0);
 | 
			
		||||
 | 
			
		||||
      const o = {};
 | 
			
		||||
      expect(reduceToCommonValue([o, o])).toEqual(o);
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        reduceToCommonValue([{ a: 1 }, { a: 1, b: 2 }], (o) => o.a),
 | 
			
		||||
      ).toEqual(1);
 | 
			
		||||
      expect(
 | 
			
		||||
        reduceToCommonValue(new Set([{ a: 1 }, { a: 1, b: 2 }]), (o) => o.a),
 | 
			
		||||
      ).toEqual(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return `null` when values are different", () => {
 | 
			
		||||
      expect(reduceToCommonValue([1, 2, 3])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue(new Set([1, 2]))).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([{ a: 1 }, { a: 2 }], (o) => o.a)).toEqual(
 | 
			
		||||
        null,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return `null` when some values are nullable", () => {
 | 
			
		||||
      expect(reduceToCommonValue([1, null, 1])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([null, 1])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([1, undefined])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([undefined, 1])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([null])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([undefined])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([])).toEqual(null);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("mapFind()", () => {
 | 
			
		||||
    it("should return the first mapped non-null element", () => {
 | 
			
		||||
      {
 | 
			
		||||
        let counter = 0;
 | 
			
		||||
 | 
			
		||||
        const result = mapFind(["a", "b", "c"], (value) => {
 | 
			
		||||
          counter++;
 | 
			
		||||
          return value === "b" ? 42 : null;
 | 
			
		||||
        });
 | 
			
		||||
        expect(result).toEqual(42);
 | 
			
		||||
        expect(counter).toBe(2);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(mapFind([1, 2], (value) => value * 0)).toBe(0);
 | 
			
		||||
      expect(mapFind([1, 2], () => false)).toBe(false);
 | 
			
		||||
      expect(mapFind([1, 2], () => "")).toBe("");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return undefined if no mapped element is found", () => {
 | 
			
		||||
      expect(mapFind([1, 2], () => undefined)).toBe(undefined);
 | 
			
		||||
      expect(mapFind([1, 2], () => null)).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,28 +1,33 @@
 | 
			
		||||
import Pool from "es6-promise-pool";
 | 
			
		||||
import { average } from "@excalidraw/math";
 | 
			
		||||
import { COLOR_PALETTE } from "./colors";
 | 
			
		||||
import type { EVENT } from "./constants";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_VERSION,
 | 
			
		||||
  FONT_FAMILY,
 | 
			
		||||
  getFontFamilyFallbacks,
 | 
			
		||||
  isDarwin,
 | 
			
		||||
  WINDOWS_EMOJI_FALLBACK_FONT,
 | 
			
		||||
} from "./constants";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawBindableElement,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  FontString,
 | 
			
		||||
} from "./element/types";
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ActiveTool,
 | 
			
		||||
  AppState,
 | 
			
		||||
  ToolType,
 | 
			
		||||
  UnsubscribeCallback,
 | 
			
		||||
  Zoom,
 | 
			
		||||
} from "./types";
 | 
			
		||||
} from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { COLOR_PALETTE } from "./colors";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_VERSION,
 | 
			
		||||
  ENV,
 | 
			
		||||
  FONT_FAMILY,
 | 
			
		||||
  getFontFamilyFallbacks,
 | 
			
		||||
  isDarwin,
 | 
			
		||||
  WINDOWS_EMOJI_FALLBACK_FONT,
 | 
			
		||||
} from "./constants";
 | 
			
		||||
 | 
			
		||||
import type { MaybePromise, ResolutionType } from "./utility-types";
 | 
			
		||||
 | 
			
		||||
import type { EVENT } from "./constants";
 | 
			
		||||
 | 
			
		||||
let mockDateTime: string | null = null;
 | 
			
		||||
 | 
			
		||||
export const setDateTimeForTests = (dateTime: string) => {
 | 
			
		||||
@@ -95,7 +100,6 @@ export const getFontFamilyString = ({
 | 
			
		||||
}) => {
 | 
			
		||||
  for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
 | 
			
		||||
    if (id === fontFamily) {
 | 
			
		||||
      // TODO: we should fallback first to generic family names first
 | 
			
		||||
      return `${fontFamilyString}${getFontFamilyFallbacks(id)
 | 
			
		||||
        .map((x) => `, ${x}`)
 | 
			
		||||
        .join("")}`;
 | 
			
		||||
@@ -167,7 +171,7 @@ export const throttleRAF = <T extends any[]>(
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const ret = (...args: T) => {
 | 
			
		||||
    if (import.meta.env.MODE === "test") {
 | 
			
		||||
    if (isTestEnv()) {
 | 
			
		||||
      fn(...args);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -380,7 +384,7 @@ export const updateActiveTool = (
 | 
			
		||||
        type: ToolType;
 | 
			
		||||
      }
 | 
			
		||||
    | { type: "custom"; customType: string }
 | 
			
		||||
  ) & { locked?: boolean }) & {
 | 
			
		||||
  ) & { locked?: boolean; fromSelection?: boolean }) & {
 | 
			
		||||
    lastActiveToolBeforeEraser?: ActiveTool | null;
 | 
			
		||||
  },
 | 
			
		||||
): AppState["activeTool"] => {
 | 
			
		||||
@@ -402,6 +406,7 @@ export const updateActiveTool = (
 | 
			
		||||
    type: data.type,
 | 
			
		||||
    customType: null,
 | 
			
		||||
    locked: data.locked ?? appState.activeTool.locked,
 | 
			
		||||
    fromSelection: data.fromSelection ?? false,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -537,6 +542,20 @@ export const findLastIndex = <T>(
 | 
			
		||||
  return -1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** returns the first non-null mapped value */
 | 
			
		||||
export const mapFind = <T, K>(
 | 
			
		||||
  collection: readonly T[],
 | 
			
		||||
  iteratee: (value: T, index: number) => K | undefined | null,
 | 
			
		||||
): K | undefined => {
 | 
			
		||||
  for (let idx = 0; idx < collection.length; idx++) {
 | 
			
		||||
    const result = iteratee(collection[idx], idx);
 | 
			
		||||
    if (result != null) {
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isTransparent = (color: string) => {
 | 
			
		||||
  const isRGBTransparent = color.length === 5 && color.substr(4, 1) === "0";
 | 
			
		||||
  const isRRGGBBTransparent = color.length === 9 && color.substr(7, 2) === "00";
 | 
			
		||||
@@ -673,7 +692,7 @@ export const arrayToMap = <T extends { id: string } | string>(
 | 
			
		||||
  return items.reduce((acc: Map<string, T>, element) => {
 | 
			
		||||
    acc.set(typeof element === "string" ? element : element.id, element);
 | 
			
		||||
    return acc;
 | 
			
		||||
  }, new Map());
 | 
			
		||||
  }, new Map() as Map<string, T>);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const arrayToMapWithIndex = <T extends { id: string }>(
 | 
			
		||||
@@ -691,8 +710,8 @@ export const arrayToObject = <T>(
 | 
			
		||||
  array: readonly T[],
 | 
			
		||||
  groupBy?: (value: T) => string | number,
 | 
			
		||||
) =>
 | 
			
		||||
  array.reduce((acc, value) => {
 | 
			
		||||
    acc[groupBy ? groupBy(value) : String(value)] = value;
 | 
			
		||||
  array.reduce((acc, value, idx) => {
 | 
			
		||||
    acc[groupBy ? groupBy(value) : idx] = value;
 | 
			
		||||
    return acc;
 | 
			
		||||
  }, {} as { [key: string]: T });
 | 
			
		||||
 | 
			
		||||
@@ -728,9 +747,30 @@ export const arrayToList = <T>(array: readonly T[]): Node<T>[] =>
 | 
			
		||||
    return acc;
 | 
			
		||||
  }, [] as Node<T>[]);
 | 
			
		||||
 | 
			
		||||
export const isTestEnv = () => import.meta.env.MODE === "test";
 | 
			
		||||
/**
 | 
			
		||||
 * Converts a readonly array or map into an iterable.
 | 
			
		||||
 * Useful for avoiding entry allocations when iterating object / map on each iteration.
 | 
			
		||||
 */
 | 
			
		||||
export const toIterable = <T>(
 | 
			
		||||
  values: readonly T[] | ReadonlyMap<string, T>,
 | 
			
		||||
): Iterable<T> => {
 | 
			
		||||
  return Array.isArray(values) ? values : values.values();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isDevEnv = () => import.meta.env.MODE === "development";
 | 
			
		||||
/**
 | 
			
		||||
 * Converts a readonly array or map into an array.
 | 
			
		||||
 */
 | 
			
		||||
export const toArray = <T>(
 | 
			
		||||
  values: readonly T[] | ReadonlyMap<string, T>,
 | 
			
		||||
): T[] => {
 | 
			
		||||
  return Array.isArray(values) ? values : Array.from(toIterable(values));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
 | 
			
		||||
 | 
			
		||||
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
 | 
			
		||||
 | 
			
		||||
export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
 | 
			
		||||
 | 
			
		||||
export const isServerEnv = () =>
 | 
			
		||||
  typeof process !== "undefined" && !!process?.env?.NODE_ENV;
 | 
			
		||||
@@ -1184,54 +1224,6 @@ export const safelyParseJSON = (json: string): Record<string, any> | null => {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
// extending the missing types
 | 
			
		||||
// relying on the [Index, T] to keep a correct order
 | 
			
		||||
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
 | 
			
		||||
  addEventListener: (
 | 
			
		||||
    type: "fulfilled",
 | 
			
		||||
    listener: (event: { data: { result: [Index, T] } }) => void,
 | 
			
		||||
  ) => (event: { data: { result: [Index, T] } }) => void;
 | 
			
		||||
  removeEventListener: (
 | 
			
		||||
    type: "fulfilled",
 | 
			
		||||
    listener: (event: { data: { result: [Index, T] } }) => void,
 | 
			
		||||
  ) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class PromisePool<T> {
 | 
			
		||||
  private readonly pool: TPromisePool<T>;
 | 
			
		||||
  private readonly entries: Record<number, T> = {};
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    source: IterableIterator<Promise<void | readonly [number, T]>>,
 | 
			
		||||
    concurrency: number,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.pool = new Pool(
 | 
			
		||||
      source as unknown as () => void | PromiseLike<[number, T][]>,
 | 
			
		||||
      concurrency,
 | 
			
		||||
    ) as TPromisePool<T>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public all() {
 | 
			
		||||
    const listener = (event: { data: { result: void | [number, T] } }) => {
 | 
			
		||||
      if (event.data.result) {
 | 
			
		||||
        // by default pool does not return the results, so we are gathering them manually
 | 
			
		||||
        // with the correct call order (represented by the index in the tuple)
 | 
			
		||||
        const [index, value] = event.data.result;
 | 
			
		||||
        this.entries[index] = value;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.pool.addEventListener("fulfilled", listener);
 | 
			
		||||
 | 
			
		||||
    return this.pool.start().then(() => {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.pool.removeEventListener("fulfilled", listener);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return Object.values(this.entries);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * use when you need to render unsafe string as HTML attribute, but MAKE SURE
 | 
			
		||||
@@ -1243,3 +1235,46 @@ export const escapeDoubleQuotes = (str: string) => {
 | 
			
		||||
 | 
			
		||||
export const castArray = <T>(value: T | T[]): T[] =>
 | 
			
		||||
  Array.isArray(value) ? value : [value];
 | 
			
		||||
 | 
			
		||||
/** hack for Array.isArray type guard not working with readonly value[] */
 | 
			
		||||
export const isReadonlyArray = (value?: any): value is readonly any[] => {
 | 
			
		||||
  return Array.isArray(value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sizeOf = (
 | 
			
		||||
  value:
 | 
			
		||||
    | readonly unknown[]
 | 
			
		||||
    | Readonly<Map<string, unknown>>
 | 
			
		||||
    | Readonly<Record<string, unknown>>
 | 
			
		||||
    | ReadonlySet<unknown>,
 | 
			
		||||
): number => {
 | 
			
		||||
  return isReadonlyArray(value)
 | 
			
		||||
    ? value.length
 | 
			
		||||
    : value instanceof Map || value instanceof Set
 | 
			
		||||
    ? value.size
 | 
			
		||||
    : Object.keys(value).length;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const reduceToCommonValue = <T, R = T>(
 | 
			
		||||
  collection: readonly T[] | ReadonlySet<T>,
 | 
			
		||||
  getValue?: (item: T) => R,
 | 
			
		||||
): R | null => {
 | 
			
		||||
  if (sizeOf(collection) === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const valueExtractor = getValue || ((item: T) => item as unknown as R);
 | 
			
		||||
 | 
			
		||||
  let commonValue: R | null = null;
 | 
			
		||||
 | 
			
		||||
  for (const item of collection) {
 | 
			
		||||
    const value = valueExtractor(item);
 | 
			
		||||
    if ((commonValue === null || commonValue === value) && value != null) {
 | 
			
		||||
      commonValue = value;
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return commonValue;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { KEYS, matchKey } from "./keys";
 | 
			
		||||
import { KEYS, matchKey } from "../src/keys";
 | 
			
		||||
 | 
			
		||||
describe("key matcher", async () => {
 | 
			
		||||
  it("should not match unexpected key", async () => {
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { Queue } from "./queue";
 | 
			
		||||
import { Queue } from "../src/queue";
 | 
			
		||||
 | 
			
		||||
describe("Queue", () => {
 | 
			
		||||
  const calls: any[] = [];
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { normalizeLink } from "./url";
 | 
			
		||||
import { normalizeLink } from "../src/url";
 | 
			
		||||
 | 
			
		||||
describe("normalizeLink", () => {
 | 
			
		||||
  // NOTE not an extensive XSS test suite, just to check if we're not
 | 
			
		||||
							
								
								
									
										8
									
								
								packages/common/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								packages/common/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "../tsconfig.base.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "outDir": "./dist/types"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*", "global.d.ts"],
 | 
			
		||||
  "exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/element/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/element/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": ["../eslintrc.base.json"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								packages/element/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/element/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# @excalidraw/element
 | 
			
		||||
 | 
			
		||||
## Install
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm install @excalidraw/element
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
yarn add @excalidraw/element
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With PNPM, similarly install the package with this command:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
pnpm add @excalidraw/element
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/element/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/element/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
/// <reference types="vite/client" />
 | 
			
		||||
import "@excalidraw/excalidraw/global";
 | 
			
		||||
import "@excalidraw/excalidraw/css";
 | 
			
		||||
							
								
								
									
										63
									
								
								packages/element/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								packages/element/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@excalidraw/element",
 | 
			
		||||
  "version": "0.18.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "types": "./dist/types/element/src/index.d.ts",
 | 
			
		||||
  "main": "./dist/prod/index.js",
 | 
			
		||||
  "module": "./dist/prod/index.js",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": {
 | 
			
		||||
      "types": "./dist/types/element/src/index.d.ts",
 | 
			
		||||
      "development": "./dist/dev/index.js",
 | 
			
		||||
      "production": "./dist/prod/index.js",
 | 
			
		||||
      "default": "./dist/prod/index.js"
 | 
			
		||||
    },
 | 
			
		||||
    "./*": {
 | 
			
		||||
      "types": "./dist/types/element/src/*.d.ts",
 | 
			
		||||
      "development": "./dist/dev/index.js",
 | 
			
		||||
      "production": "./dist/prod/index.js",
 | 
			
		||||
      "default": "./dist/prod/index.js"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "files": [
 | 
			
		||||
    "dist/*"
 | 
			
		||||
  ],
 | 
			
		||||
  "description": "Excalidraw elements-related logic",
 | 
			
		||||
  "publishConfig": {
 | 
			
		||||
    "access": "public"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "excalidraw",
 | 
			
		||||
    "excalidraw-utils"
 | 
			
		||||
  ],
 | 
			
		||||
  "browserslist": {
 | 
			
		||||
    "production": [
 | 
			
		||||
      ">0.2%",
 | 
			
		||||
      "not dead",
 | 
			
		||||
      "not ie <= 11",
 | 
			
		||||
      "not op_mini all",
 | 
			
		||||
      "not safari < 12",
 | 
			
		||||
      "not kaios <= 2.5",
 | 
			
		||||
      "not edge < 79",
 | 
			
		||||
      "not chrome < 70",
 | 
			
		||||
      "not and_uc < 13",
 | 
			
		||||
      "not samsung < 10"
 | 
			
		||||
    ],
 | 
			
		||||
    "development": [
 | 
			
		||||
      "last 1 chrome version",
 | 
			
		||||
      "last 1 firefox version",
 | 
			
		||||
      "last 1 safari version"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "bugs": "https://github.com/excalidraw/excalidraw/issues",
 | 
			
		||||
  "repository": "https://github.com/excalidraw/excalidraw",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "gen:types": "rimraf types && tsc",
 | 
			
		||||
    "build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@excalidraw/common": "0.18.0",
 | 
			
		||||
    "@excalidraw/math": "0.18.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,27 @@
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  randomInteger,
 | 
			
		||||
  arrayToMap,
 | 
			
		||||
  toBrandedType,
 | 
			
		||||
  isDevEnv,
 | 
			
		||||
  isTestEnv,
 | 
			
		||||
  toArray,
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
import { isNonDeletedElement } from "@excalidraw/element";
 | 
			
		||||
import { isFrameLikeElement } from "@excalidraw/element";
 | 
			
		||||
import { getElementsInGroup } from "@excalidraw/element";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  syncInvalidIndices,
 | 
			
		||||
  syncMovedIndices,
 | 
			
		||||
  validateFractionalIndices,
 | 
			
		||||
} from "@excalidraw/element";
 | 
			
		||||
 | 
			
		||||
import { getSelectedElements } from "@excalidraw/element";
 | 
			
		||||
 | 
			
		||||
import { mutateElement, type ElementUpdate } from "@excalidraw/element";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
@@ -9,26 +32,15 @@ import type {
 | 
			
		||||
  NonDeletedSceneElementsMap,
 | 
			
		||||
  OrderedExcalidrawElement,
 | 
			
		||||
  Ordered,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
import { isNonDeletedElement } from "../element";
 | 
			
		||||
import type { LinearElementEditor } from "../element/linearElementEditor";
 | 
			
		||||
import { isFrameLikeElement } from "../element/typeChecks";
 | 
			
		||||
import { getSelectedElements } from "./selection";
 | 
			
		||||
import type { AppState } from "../types";
 | 
			
		||||
import type { Assert, SameType } from "../utility-types";
 | 
			
		||||
import { randomInteger } from "../random";
 | 
			
		||||
import {
 | 
			
		||||
  syncInvalidIndices,
 | 
			
		||||
  syncMovedIndices,
 | 
			
		||||
  validateFractionalIndices,
 | 
			
		||||
} from "../fractionalIndex";
 | 
			
		||||
import { arrayToMap } from "../utils";
 | 
			
		||||
import { toBrandedType } from "../utils";
 | 
			
		||||
import { ENV } from "../constants";
 | 
			
		||||
import { getElementsInGroup } from "../groups";
 | 
			
		||||
} from "@excalidraw/element/types";
 | 
			
		||||
 | 
			
		||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
 | 
			
		||||
type ElementKey = ExcalidrawElement | ElementIdKey;
 | 
			
		||||
import type {
 | 
			
		||||
  Assert,
 | 
			
		||||
  Mutable,
 | 
			
		||||
  SameType,
 | 
			
		||||
} from "@excalidraw/common/utility-types";
 | 
			
		||||
 | 
			
		||||
import type { AppState } from "../../excalidraw/types";
 | 
			
		||||
 | 
			
		||||
type SceneStateCallback = () => void;
 | 
			
		||||
type SceneStateCallbackRemover = () => void;
 | 
			
		||||
@@ -54,14 +66,10 @@ const getNonDeletedElements = <T extends ExcalidrawElement>(
 | 
			
		||||
 | 
			
		||||
const validateIndicesThrottled = throttle(
 | 
			
		||||
  (elements: readonly ExcalidrawElement[]) => {
 | 
			
		||||
    if (
 | 
			
		||||
      import.meta.env.DEV ||
 | 
			
		||||
      import.meta.env.MODE === ENV.TEST ||
 | 
			
		||||
      window?.DEBUG_FRACTIONAL_INDICES
 | 
			
		||||
    ) {
 | 
			
		||||
    if (isDevEnv() || isTestEnv() || window?.DEBUG_FRACTIONAL_INDICES) {
 | 
			
		||||
      validateFractionalIndices(elements, {
 | 
			
		||||
        // throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
 | 
			
		||||
        shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
 | 
			
		||||
        shouldThrow: isDevEnv() || isTestEnv(),
 | 
			
		||||
        includeBoundTextValidation: true,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
@@ -97,44 +105,7 @@ const hashSelectionOpts = (
 | 
			
		||||
// in our codebase
 | 
			
		||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
 | 
			
		||||
 | 
			
		||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
 | 
			
		||||
  if (typeof elementKey === "string") {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Scene {
 | 
			
		||||
  // ---------------------------------------------------------------------------
 | 
			
		||||
  // static methods/props
 | 
			
		||||
  // ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
  private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
 | 
			
		||||
  private static sceneMapById = new Map<string, Scene>();
 | 
			
		||||
 | 
			
		||||
  static mapElementToScene(elementKey: ElementKey, scene: Scene) {
 | 
			
		||||
    if (isIdKey(elementKey)) {
 | 
			
		||||
      // for cases where we don't have access to the element object
 | 
			
		||||
      // (e.g. restore serialized appState with id references)
 | 
			
		||||
      this.sceneMapById.set(elementKey, scene);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.sceneMapByElement.set(elementKey, scene);
 | 
			
		||||
      // if mapping element objects, also cache the id string when later
 | 
			
		||||
      // looking up by id alone
 | 
			
		||||
      this.sceneMapById.set(elementKey.id, scene);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @deprecated pass down `app.scene` and use it directly
 | 
			
		||||
   */
 | 
			
		||||
  static getScene(elementKey: ElementKey): Scene | null {
 | 
			
		||||
    if (isIdKey(elementKey)) {
 | 
			
		||||
      return this.sceneMapById.get(elementKey) || null;
 | 
			
		||||
    }
 | 
			
		||||
    return this.sceneMapByElement.get(elementKey) || null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
export class Scene {
 | 
			
		||||
  // ---------------------------------------------------------------------------
 | 
			
		||||
  // instance methods/props
 | 
			
		||||
  // ---------------------------------------------------------------------------
 | 
			
		||||
@@ -193,6 +164,12 @@ class Scene {
 | 
			
		||||
    return this.frames;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(elements: ElementsMapOrArray | null = null) {
 | 
			
		||||
    if (elements) {
 | 
			
		||||
      this.replaceAllElements(elements);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getSelectedElements(opts: {
 | 
			
		||||
    // NOTE can be ommitted by making Scene constructor require App instance
 | 
			
		||||
    selectedElementIds: AppState["selectedElementIds"];
 | 
			
		||||
@@ -287,11 +264,8 @@ class Scene {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  replaceAllElements(nextElements: ElementsMapOrArray) {
 | 
			
		||||
    const _nextElements =
 | 
			
		||||
      // ts doesn't like `Array.isArray` of `instanceof Map`
 | 
			
		||||
      nextElements instanceof Array
 | 
			
		||||
        ? nextElements
 | 
			
		||||
        : Array.from(nextElements.values());
 | 
			
		||||
    // we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
 | 
			
		||||
    const _nextElements = toArray(nextElements);
 | 
			
		||||
    const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
 | 
			
		||||
 | 
			
		||||
    validateIndicesThrottled(_nextElements);
 | 
			
		||||
@@ -303,7 +277,6 @@ class Scene {
 | 
			
		||||
        nextFrameLikes.push(element);
 | 
			
		||||
      }
 | 
			
		||||
      this.elementsMap.set(element.id, element);
 | 
			
		||||
      Scene.mapElementToScene(element, this);
 | 
			
		||||
    });
 | 
			
		||||
    const nonDeletedElements = getNonDeletedElements(this.elements);
 | 
			
		||||
    this.nonDeletedElements = nonDeletedElements.elements;
 | 
			
		||||
@@ -348,12 +321,6 @@ class Scene {
 | 
			
		||||
    this.selectedElementsCache.elements = null;
 | 
			
		||||
    this.selectedElementsCache.cache.clear();
 | 
			
		||||
 | 
			
		||||
    Scene.sceneMapById.forEach((scene, elementKey) => {
 | 
			
		||||
      if (scene === this) {
 | 
			
		||||
        Scene.sceneMapById.delete(elementKey);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // done not for memory leaks, but to guard against possible late fires
 | 
			
		||||
    // (I guess?)
 | 
			
		||||
    this.callbacks.clear();
 | 
			
		||||
@@ -450,6 +417,40 @@ class Scene {
 | 
			
		||||
    // then, check if the id is a group
 | 
			
		||||
    return getElementsInGroup(elementsMap, id);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Scene;
 | 
			
		||||
  // Mutate an element with passed updates and trigger the component to update. Make sure you
 | 
			
		||||
  // are calling it either from a React event handler or within unstable_batchedUpdates().
 | 
			
		||||
  mutateElement<TElement extends Mutable<ExcalidrawElement>>(
 | 
			
		||||
    element: TElement,
 | 
			
		||||
    updates: ElementUpdate<TElement>,
 | 
			
		||||
    options: {
 | 
			
		||||
      informMutation: boolean;
 | 
			
		||||
      isDragging: boolean;
 | 
			
		||||
    } = {
 | 
			
		||||
      informMutation: true,
 | 
			
		||||
      isDragging: false,
 | 
			
		||||
    },
 | 
			
		||||
  ) {
 | 
			
		||||
    const elementsMap = this.getNonDeletedElementsMap();
 | 
			
		||||
 | 
			
		||||
    const { version: prevVersion } = element;
 | 
			
		||||
    const { version: nextVersion } = mutateElement(
 | 
			
		||||
      element,
 | 
			
		||||
      elementsMap,
 | 
			
		||||
      updates,
 | 
			
		||||
      options,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      // skip if the element is not in the scene (i.e. selection)
 | 
			
		||||
      this.elementsMap.has(element.id) &&
 | 
			
		||||
      // skip if the element's version hasn't changed, as mutateElement returned the same element
 | 
			
		||||
      prevVersion !== nextVersion &&
 | 
			
		||||
      options.informMutation
 | 
			
		||||
    ) {
 | 
			
		||||
      this.triggerUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return element;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,13 @@
 | 
			
		||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
 | 
			
		||||
import { mutateElement } from "./element/mutateElement";
 | 
			
		||||
import type { BoundingBox } from "./element/bounds";
 | 
			
		||||
import { getCommonBoundingBox } from "./element/bounds";
 | 
			
		||||
import { getMaximumGroups } from "./groups";
 | 
			
		||||
import { updateBoundElements } from "./element/binding";
 | 
			
		||||
import type Scene from "./scene/Scene";
 | 
			
		||||
import type { AppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { updateBoundElements } from "./binding";
 | 
			
		||||
import { getCommonBoundingBox } from "./bounds";
 | 
			
		||||
import { getSelectedElementsByGroup } from "./groups";
 | 
			
		||||
 | 
			
		||||
import type { Scene } from "./Scene";
 | 
			
		||||
 | 
			
		||||
import type { BoundingBox } from "./bounds";
 | 
			
		||||
import type { ExcalidrawElement } from "./types";
 | 
			
		||||
 | 
			
		||||
export interface Alignment {
 | 
			
		||||
  position: "start" | "center" | "end";
 | 
			
		||||
@@ -13,13 +16,14 @@ export interface Alignment {
 | 
			
		||||
 | 
			
		||||
export const alignElements = (
 | 
			
		||||
  selectedElements: ExcalidrawElement[],
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  alignment: Alignment,
 | 
			
		||||
  scene: Scene,
 | 
			
		||||
  appState: Readonly<AppState>,
 | 
			
		||||
): ExcalidrawElement[] => {
 | 
			
		||||
  const groups: ExcalidrawElement[][] = getMaximumGroups(
 | 
			
		||||
  const groups: ExcalidrawElement[][] = getSelectedElementsByGroup(
 | 
			
		||||
    selectedElements,
 | 
			
		||||
    elementsMap,
 | 
			
		||||
    scene.getNonDeletedElementsMap(),
 | 
			
		||||
    appState,
 | 
			
		||||
  );
 | 
			
		||||
  const selectionBoundingBox = getCommonBoundingBox(selectedElements);
 | 
			
		||||
 | 
			
		||||
@@ -31,12 +35,13 @@ export const alignElements = (
 | 
			
		||||
    );
 | 
			
		||||
    return group.map((element) => {
 | 
			
		||||
      // update element
 | 
			
		||||
      const updatedEle = mutateElement(element, {
 | 
			
		||||
      const updatedEle = scene.mutateElement(element, {
 | 
			
		||||
        x: element.x + translation.x,
 | 
			
		||||
        y: element.y + translation.y,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // update bound elements
 | 
			
		||||
      updateBoundElements(element, scene.getNonDeletedElementsMap(), {
 | 
			
		||||
      updateBoundElements(element, scene, {
 | 
			
		||||
        simultaneouslyUpdated: group,
 | 
			
		||||
      });
 | 
			
		||||
      return updatedEle;
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,17 +1,42 @@
 | 
			
		||||
import type {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawLinearElement,
 | 
			
		||||
  Arrowhead,
 | 
			
		||||
  ExcalidrawFreeDrawElement,
 | 
			
		||||
  NonDeleted,
 | 
			
		||||
  ExcalidrawTextElementWithContainer,
 | 
			
		||||
  ElementsMap,
 | 
			
		||||
} from "./types";
 | 
			
		||||
import rough from "roughjs/bin/rough";
 | 
			
		||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
 | 
			
		||||
import type { Drawable, Op } from "roughjs/bin/core";
 | 
			
		||||
import type { AppState } from "../types";
 | 
			
		||||
import { generateRoughOptions } from "../scene/Shape";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  arrayToMap,
 | 
			
		||||
  invariant,
 | 
			
		||||
  rescalePoints,
 | 
			
		||||
  sizeOf,
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  degreesToRadians,
 | 
			
		||||
  lineSegment,
 | 
			
		||||
  pointDistance,
 | 
			
		||||
  pointFrom,
 | 
			
		||||
  pointFromArray,
 | 
			
		||||
  pointRotateRads,
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
 | 
			
		||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
 | 
			
		||||
 | 
			
		||||
import { pointsOnBezierCurves } from "points-on-curve";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  Curve,
 | 
			
		||||
  Degrees,
 | 
			
		||||
  GlobalPoint,
 | 
			
		||||
  LineSegment,
 | 
			
		||||
  LocalPoint,
 | 
			
		||||
  Radians,
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
 | 
			
		||||
import type { AppState } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import type { Mutable } from "@excalidraw/common/utility-types";
 | 
			
		||||
 | 
			
		||||
import { generateRoughOptions } from "./shape";
 | 
			
		||||
import { ShapeCache } from "./shape";
 | 
			
		||||
import { LinearElementEditor } from "./linearElementEditor";
 | 
			
		||||
import { getBoundTextElement, getContainerElement } from "./textElement";
 | 
			
		||||
import {
 | 
			
		||||
  isArrowElement,
 | 
			
		||||
  isBoundToContainer,
 | 
			
		||||
@@ -19,28 +44,28 @@ import {
 | 
			
		||||
  isLinearElement,
 | 
			
		||||
  isTextElement,
 | 
			
		||||
} from "./typeChecks";
 | 
			
		||||
import { rescalePoints } from "../points";
 | 
			
		||||
import { getBoundTextElement, getContainerElement } from "./textElement";
 | 
			
		||||
import { LinearElementEditor } from "./linearElementEditor";
 | 
			
		||||
import { ShapeCache } from "../scene/ShapeCache";
 | 
			
		||||
import { arrayToMap, invariant } from "../utils";
 | 
			
		||||
import type {
 | 
			
		||||
  Degrees,
 | 
			
		||||
  GlobalPoint,
 | 
			
		||||
  LineSegment,
 | 
			
		||||
  LocalPoint,
 | 
			
		||||
  Radians,
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
 | 
			
		||||
import { getElementShape } from "./shape";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  degreesToRadians,
 | 
			
		||||
  lineSegment,
 | 
			
		||||
  pointFrom,
 | 
			
		||||
  pointDistance,
 | 
			
		||||
  pointFromArray,
 | 
			
		||||
  pointRotateRads,
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
import type { Mutable } from "../utility-types";
 | 
			
		||||
import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
 | 
			
		||||
  deconstructDiamondElement,
 | 
			
		||||
  deconstructRectanguloidElement,
 | 
			
		||||
} from "./utils";
 | 
			
		||||
 | 
			
		||||
import type { Drawable, Op } from "roughjs/bin/core";
 | 
			
		||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
 | 
			
		||||
import type {
 | 
			
		||||
  Arrowhead,
 | 
			
		||||
  ElementsMap,
 | 
			
		||||
  ElementsMapOrArray,
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawEllipseElement,
 | 
			
		||||
  ExcalidrawFreeDrawElement,
 | 
			
		||||
  ExcalidrawLinearElement,
 | 
			
		||||
  ExcalidrawRectanguloidElement,
 | 
			
		||||
  ExcalidrawTextElementWithContainer,
 | 
			
		||||
  NonDeleted,
 | 
			
		||||
} from "./types";
 | 
			
		||||
 | 
			
		||||
export type RectangleBox = {
 | 
			
		||||
  x: number;
 | 
			
		||||
@@ -77,9 +102,23 @@ export class ElementBounds {
 | 
			
		||||
      version: ExcalidrawElement["version"];
 | 
			
		||||
    }
 | 
			
		||||
  >();
 | 
			
		||||
  private static nonRotatedBoundsCache = new WeakMap<
 | 
			
		||||
    ExcalidrawElement,
 | 
			
		||||
    {
 | 
			
		||||
      bounds: Bounds;
 | 
			
		||||
      version: ExcalidrawElement["version"];
 | 
			
		||||
    }
 | 
			
		||||
  >();
 | 
			
		||||
 | 
			
		||||
  static getBounds(element: ExcalidrawElement, elementsMap: ElementsMap) {
 | 
			
		||||
    const cachedBounds = ElementBounds.boundsCache.get(element);
 | 
			
		||||
  static getBounds(
 | 
			
		||||
    element: ExcalidrawElement,
 | 
			
		||||
    elementsMap: ElementsMap,
 | 
			
		||||
    nonRotated: boolean = false,
 | 
			
		||||
  ) {
 | 
			
		||||
    const cachedBounds =
 | 
			
		||||
      nonRotated && element.angle !== 0
 | 
			
		||||
        ? ElementBounds.nonRotatedBoundsCache.get(element)
 | 
			
		||||
        : ElementBounds.boundsCache.get(element);
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      cachedBounds?.version &&
 | 
			
		||||
@@ -90,6 +129,23 @@ export class ElementBounds {
 | 
			
		||||
    ) {
 | 
			
		||||
      return cachedBounds.bounds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nonRotated && element.angle !== 0) {
 | 
			
		||||
      const nonRotatedBounds = ElementBounds.calculateBounds(
 | 
			
		||||
        {
 | 
			
		||||
          ...element,
 | 
			
		||||
          angle: 0 as Radians,
 | 
			
		||||
        },
 | 
			
		||||
        elementsMap,
 | 
			
		||||
      );
 | 
			
		||||
      ElementBounds.nonRotatedBoundsCache.set(element, {
 | 
			
		||||
        version: element.version,
 | 
			
		||||
        bounds: nonRotatedBounds,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return nonRotatedBounds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const bounds = ElementBounds.calculateBounds(element, elementsMap);
 | 
			
		||||
 | 
			
		||||
    ElementBounds.boundsCache.set(element, {
 | 
			
		||||
@@ -247,50 +303,82 @@ export const getElementAbsoluteCoords = (
 | 
			
		||||
 * that can be used for visual collision detection (useful for frames)
 | 
			
		||||
 * as opposed to bounding box collision detection
 | 
			
		||||
 */
 | 
			
		||||
/**
 | 
			
		||||
 * Given an element, return the line segments that make up the element.
 | 
			
		||||
 *
 | 
			
		||||
 * Uses helpers from /math
 | 
			
		||||
 */
 | 
			
		||||
export const getElementLineSegments = (
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
): LineSegment<GlobalPoint>[] => {
 | 
			
		||||
  const shape = getElementShape(element, elementsMap);
 | 
			
		||||
  const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
 | 
			
		||||
    element,
 | 
			
		||||
    elementsMap,
 | 
			
		||||
  );
 | 
			
		||||
  const center = pointFrom<GlobalPoint>(cx, cy);
 | 
			
		||||
 | 
			
		||||
  const center: GlobalPoint = pointFrom(cx, cy);
 | 
			
		||||
 | 
			
		||||
  if (isLinearElement(element) || isFreeDrawElement(element)) {
 | 
			
		||||
    const segments: LineSegment<GlobalPoint>[] = [];
 | 
			
		||||
 | 
			
		||||
  if (shape.type === "polycurve") {
 | 
			
		||||
    const curves = shape.data;
 | 
			
		||||
    const points = curves
 | 
			
		||||
      .map((curve) => pointsOnBezierCurves(curve, 10))
 | 
			
		||||
      .flat();
 | 
			
		||||
    let i = 0;
 | 
			
		||||
 | 
			
		||||
    while (i < element.points.length - 1) {
 | 
			
		||||
    const segments: LineSegment<GlobalPoint>[] = [];
 | 
			
		||||
    while (i < points.length - 1) {
 | 
			
		||||
      segments.push(
 | 
			
		||||
        lineSegment(
 | 
			
		||||
          pointRotateRads(
 | 
			
		||||
            pointFrom(
 | 
			
		||||
              element.points[i][0] + element.x,
 | 
			
		||||
              element.points[i][1] + element.y,
 | 
			
		||||
            ),
 | 
			
		||||
            center,
 | 
			
		||||
            element.angle,
 | 
			
		||||
          ),
 | 
			
		||||
          pointRotateRads(
 | 
			
		||||
            pointFrom(
 | 
			
		||||
              element.points[i + 1][0] + element.x,
 | 
			
		||||
              element.points[i + 1][1] + element.y,
 | 
			
		||||
            ),
 | 
			
		||||
            center,
 | 
			
		||||
            element.angle,
 | 
			
		||||
          ),
 | 
			
		||||
          pointFrom(points[i][0], points[i][1]),
 | 
			
		||||
          pointFrom(points[i + 1][0], points[i + 1][1]),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      i++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return segments;
 | 
			
		||||
  } else if (shape.type === "polyline") {
 | 
			
		||||
    return shape.data as LineSegment<GlobalPoint>[];
 | 
			
		||||
  } else if (_isRectanguloidElement(element)) {
 | 
			
		||||
    const [sides, corners] = deconstructRectanguloidElement(element);
 | 
			
		||||
    const cornerSegments: LineSegment<GlobalPoint>[] = corners
 | 
			
		||||
      .map((corner) => getSegmentsOnCurve(corner, center, element.angle))
 | 
			
		||||
      .flat();
 | 
			
		||||
    const rotatedSides = getRotatedSides(sides, center, element.angle);
 | 
			
		||||
    return [...rotatedSides, ...cornerSegments];
 | 
			
		||||
  } else if (element.type === "diamond") {
 | 
			
		||||
    const [sides, corners] = deconstructDiamondElement(element);
 | 
			
		||||
    const cornerSegments = corners
 | 
			
		||||
      .map((corner) => getSegmentsOnCurve(corner, center, element.angle))
 | 
			
		||||
      .flat();
 | 
			
		||||
    const rotatedSides = getRotatedSides(sides, center, element.angle);
 | 
			
		||||
 | 
			
		||||
    return [...rotatedSides, ...cornerSegments];
 | 
			
		||||
  } else if (shape.type === "polygon") {
 | 
			
		||||
    if (isTextElement(element)) {
 | 
			
		||||
      const container = getContainerElement(element, elementsMap);
 | 
			
		||||
      if (container && isLinearElement(container)) {
 | 
			
		||||
        const segments: LineSegment<GlobalPoint>[] = [
 | 
			
		||||
          lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
 | 
			
		||||
          lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
 | 
			
		||||
          lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
 | 
			
		||||
          lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
 | 
			
		||||
        ];
 | 
			
		||||
        return segments;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const points = shape.data as GlobalPoint[];
 | 
			
		||||
    const segments: LineSegment<GlobalPoint>[] = [];
 | 
			
		||||
    for (let i = 0; i < points.length - 1; i++) {
 | 
			
		||||
      segments.push(lineSegment(points[i], points[i + 1]));
 | 
			
		||||
    }
 | 
			
		||||
    return segments;
 | 
			
		||||
  } else if (shape.type === "ellipse") {
 | 
			
		||||
    return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [nw, ne, sw, se, n, s, w, e] = (
 | 
			
		||||
  const [nw, ne, sw, se, , , w, e] = (
 | 
			
		||||
    [
 | 
			
		||||
      [x1, y1],
 | 
			
		||||
      [x2, y1],
 | 
			
		||||
@@ -303,28 +391,6 @@ export const getElementLineSegments = (
 | 
			
		||||
    ] as GlobalPoint[]
 | 
			
		||||
  ).map((point) => pointRotateRads(point, center, element.angle));
 | 
			
		||||
 | 
			
		||||
  if (element.type === "diamond") {
 | 
			
		||||
    return [
 | 
			
		||||
      lineSegment(n, w),
 | 
			
		||||
      lineSegment(n, e),
 | 
			
		||||
      lineSegment(s, w),
 | 
			
		||||
      lineSegment(s, e),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (element.type === "ellipse") {
 | 
			
		||||
    return [
 | 
			
		||||
      lineSegment(n, w),
 | 
			
		||||
      lineSegment(n, e),
 | 
			
		||||
      lineSegment(s, w),
 | 
			
		||||
      lineSegment(s, e),
 | 
			
		||||
      lineSegment(n, w),
 | 
			
		||||
      lineSegment(n, e),
 | 
			
		||||
      lineSegment(s, w),
 | 
			
		||||
      lineSegment(s, e),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    lineSegment(nw, ne),
 | 
			
		||||
    lineSegment(sw, se),
 | 
			
		||||
@@ -337,6 +403,94 @@ export const getElementLineSegments = (
 | 
			
		||||
  ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const _isRectanguloidElement = (
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
): element is ExcalidrawRectanguloidElement => {
 | 
			
		||||
  return (
 | 
			
		||||
    element != null &&
 | 
			
		||||
    (element.type === "rectangle" ||
 | 
			
		||||
      element.type === "image" ||
 | 
			
		||||
      element.type === "iframe" ||
 | 
			
		||||
      element.type === "embeddable" ||
 | 
			
		||||
      element.type === "frame" ||
 | 
			
		||||
      element.type === "magicframe" ||
 | 
			
		||||
      (element.type === "text" && !element.containerId))
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getRotatedSides = (
 | 
			
		||||
  sides: LineSegment<GlobalPoint>[],
 | 
			
		||||
  center: GlobalPoint,
 | 
			
		||||
  angle: Radians,
 | 
			
		||||
) => {
 | 
			
		||||
  return sides.map((side) => {
 | 
			
		||||
    return lineSegment(
 | 
			
		||||
      pointRotateRads<GlobalPoint>(side[0], center, angle),
 | 
			
		||||
      pointRotateRads<GlobalPoint>(side[1], center, angle),
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getSegmentsOnCurve = (
 | 
			
		||||
  curve: Curve<GlobalPoint>,
 | 
			
		||||
  center: GlobalPoint,
 | 
			
		||||
  angle: Radians,
 | 
			
		||||
): LineSegment<GlobalPoint>[] => {
 | 
			
		||||
  const points = pointsOnBezierCurves(curve, 10);
 | 
			
		||||
  let i = 0;
 | 
			
		||||
  const segments: LineSegment<GlobalPoint>[] = [];
 | 
			
		||||
  while (i < points.length - 1) {
 | 
			
		||||
    segments.push(
 | 
			
		||||
      lineSegment(
 | 
			
		||||
        pointRotateRads<GlobalPoint>(
 | 
			
		||||
          pointFrom(points[i][0], points[i][1]),
 | 
			
		||||
          center,
 | 
			
		||||
          angle,
 | 
			
		||||
        ),
 | 
			
		||||
        pointRotateRads<GlobalPoint>(
 | 
			
		||||
          pointFrom(points[i + 1][0], points[i + 1][1]),
 | 
			
		||||
          center,
 | 
			
		||||
          angle,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    i++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return segments;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getSegmentsOnEllipse = (
 | 
			
		||||
  ellipse: ExcalidrawEllipseElement,
 | 
			
		||||
): LineSegment<GlobalPoint>[] => {
 | 
			
		||||
  const center = pointFrom<GlobalPoint>(
 | 
			
		||||
    ellipse.x + ellipse.width / 2,
 | 
			
		||||
    ellipse.y + ellipse.height / 2,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const a = ellipse.width / 2;
 | 
			
		||||
  const b = ellipse.height / 2;
 | 
			
		||||
 | 
			
		||||
  const segments: LineSegment<GlobalPoint>[] = [];
 | 
			
		||||
  const points: GlobalPoint[] = [];
 | 
			
		||||
  const n = 90;
 | 
			
		||||
  const deltaT = (Math.PI * 2) / n;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < n; i++) {
 | 
			
		||||
    const t = i * deltaT;
 | 
			
		||||
    const x = center[0] + a * Math.cos(t);
 | 
			
		||||
    const y = center[1] + b * Math.sin(t);
 | 
			
		||||
    points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < points.length - 1; i++) {
 | 
			
		||||
    segments.push(lineSegment(points[i], points[i + 1]));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  segments.push(lineSegment(points[points.length - 1], points[0]));
 | 
			
		||||
  return segments;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Scene -> Scene coords, but in x1,x2,y1,y2 format.
 | 
			
		||||
 *
 | 
			
		||||
@@ -430,7 +584,7 @@ const solveQuadratic = (
 | 
			
		||||
  return [s1, s2];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getCubicBezierCurveBound = (
 | 
			
		||||
export const getCubicBezierCurveBound = (
 | 
			
		||||
  p0: GlobalPoint,
 | 
			
		||||
  p1: GlobalPoint,
 | 
			
		||||
  p2: GlobalPoint,
 | 
			
		||||
@@ -816,15 +970,16 @@ const getLinearElementRotatedBounds = (
 | 
			
		||||
export const getElementBounds = (
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  nonRotated: boolean = false,
 | 
			
		||||
): Bounds => {
 | 
			
		||||
  return ElementBounds.getBounds(element, elementsMap);
 | 
			
		||||
  return ElementBounds.getBounds(element, elementsMap, nonRotated);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getCommonBounds = (
 | 
			
		||||
  elements: readonly ExcalidrawElement[],
 | 
			
		||||
  elements: ElementsMapOrArray,
 | 
			
		||||
  elementsMap?: ElementsMap,
 | 
			
		||||
): Bounds => {
 | 
			
		||||
  if (!elements.length) {
 | 
			
		||||
  if (!sizeOf(elements)) {
 | 
			
		||||
    return [0, 0, 0, 0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1010,6 +1165,71 @@ export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
 | 
			
		||||
    bounds[1] + (bounds[3] - bounds[1]) / 2,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get the axis-aligned bounding box for a given element
 | 
			
		||||
 */
 | 
			
		||||
export const aabbForElement = (
 | 
			
		||||
  element: Readonly<ExcalidrawElement>,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  offset?: [number, number, number, number],
 | 
			
		||||
) => {
 | 
			
		||||
  const bbox = {
 | 
			
		||||
    minX: element.x,
 | 
			
		||||
    minY: element.y,
 | 
			
		||||
    maxX: element.x + element.width,
 | 
			
		||||
    maxY: element.y + element.height,
 | 
			
		||||
    midX: element.x + element.width / 2,
 | 
			
		||||
    midY: element.y + element.height / 2,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const center = elementCenterPoint(element, elementsMap);
 | 
			
		||||
  const [topLeftX, topLeftY] = pointRotateRads(
 | 
			
		||||
    pointFrom(bbox.minX, bbox.minY),
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
  );
 | 
			
		||||
  const [topRightX, topRightY] = pointRotateRads(
 | 
			
		||||
    pointFrom(bbox.maxX, bbox.minY),
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
  );
 | 
			
		||||
  const [bottomRightX, bottomRightY] = pointRotateRads(
 | 
			
		||||
    pointFrom(bbox.maxX, bbox.maxY),
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
  );
 | 
			
		||||
  const [bottomLeftX, bottomLeftY] = pointRotateRads(
 | 
			
		||||
    pointFrom(bbox.minX, bbox.maxY),
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const bounds = [
 | 
			
		||||
    Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX),
 | 
			
		||||
    Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY),
 | 
			
		||||
    Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX),
 | 
			
		||||
    Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY),
 | 
			
		||||
  ] as Bounds;
 | 
			
		||||
 | 
			
		||||
  if (offset) {
 | 
			
		||||
    const [topOffset, rightOffset, downOffset, leftOffset] = offset;
 | 
			
		||||
    return [
 | 
			
		||||
      bounds[0] - leftOffset,
 | 
			
		||||
      bounds[1] - topOffset,
 | 
			
		||||
      bounds[2] + rightOffset,
 | 
			
		||||
      bounds[3] + downOffset,
 | 
			
		||||
    ] as Bounds;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return bounds;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
 | 
			
		||||
  p: P,
 | 
			
		||||
  bounds: Bounds,
 | 
			
		||||
): boolean =>
 | 
			
		||||
  p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
 | 
			
		||||
 | 
			
		||||
export const doBoundsIntersect = (
 | 
			
		||||
  bounds1: Bounds | null,
 | 
			
		||||
  bounds2: Bounds | null,
 | 
			
		||||
@@ -1023,3 +1243,14 @@ export const doBoundsIntersect = (
 | 
			
		||||
 | 
			
		||||
  return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const elementCenterPoint = (
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  xOffset: number = 0,
 | 
			
		||||
  yOffset: number = 0,
 | 
			
		||||
) => {
 | 
			
		||||
  const [x, y] = getCenterForBounds(getElementBounds(element, elementsMap));
 | 
			
		||||
 | 
			
		||||
  return pointFrom<GlobalPoint>(x + xOffset, y + yOffset);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										556
									
								
								packages/element/src/collision.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										556
									
								
								packages/element/src/collision.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,556 @@
 | 
			
		||||
import { isTransparent } from "@excalidraw/common";
 | 
			
		||||
import {
 | 
			
		||||
  curveIntersectLineSegment,
 | 
			
		||||
  isPointWithinBounds,
 | 
			
		||||
  lineSegment,
 | 
			
		||||
  lineSegmentIntersectionPoints,
 | 
			
		||||
  pointFrom,
 | 
			
		||||
  pointFromVector,
 | 
			
		||||
  pointRotateRads,
 | 
			
		||||
  pointsEqual,
 | 
			
		||||
  vectorFromPoint,
 | 
			
		||||
  vectorNormalize,
 | 
			
		||||
  vectorScale,
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ellipse,
 | 
			
		||||
  ellipseSegmentInterceptPoints,
 | 
			
		||||
} from "@excalidraw/math/ellipse";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  Curve,
 | 
			
		||||
  GlobalPoint,
 | 
			
		||||
  LineSegment,
 | 
			
		||||
  Radians,
 | 
			
		||||
} from "@excalidraw/math";
 | 
			
		||||
 | 
			
		||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
import { isPathALoop } from "./utils";
 | 
			
		||||
import {
 | 
			
		||||
  type Bounds,
 | 
			
		||||
  doBoundsIntersect,
 | 
			
		||||
  elementCenterPoint,
 | 
			
		||||
  getCenterForBounds,
 | 
			
		||||
  getCubicBezierCurveBound,
 | 
			
		||||
  getElementBounds,
 | 
			
		||||
} from "./bounds";
 | 
			
		||||
import {
 | 
			
		||||
  hasBoundTextElement,
 | 
			
		||||
  isFreeDrawElement,
 | 
			
		||||
  isIframeLikeElement,
 | 
			
		||||
  isImageElement,
 | 
			
		||||
  isLinearElement,
 | 
			
		||||
  isTextElement,
 | 
			
		||||
} from "./typeChecks";
 | 
			
		||||
import {
 | 
			
		||||
  deconstructDiamondElement,
 | 
			
		||||
  deconstructLinearOrFreeDrawElement,
 | 
			
		||||
  deconstructRectanguloidElement,
 | 
			
		||||
} from "./utils";
 | 
			
		||||
 | 
			
		||||
import { getBoundTextElement } from "./textElement";
 | 
			
		||||
 | 
			
		||||
import { LinearElementEditor } from "./linearElementEditor";
 | 
			
		||||
 | 
			
		||||
import { distanceToElement } from "./distance";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ElementsMap,
 | 
			
		||||
  ExcalidrawDiamondElement,
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawEllipseElement,
 | 
			
		||||
  ExcalidrawFreeDrawElement,
 | 
			
		||||
  ExcalidrawLinearElement,
 | 
			
		||||
  ExcalidrawRectanguloidElement,
 | 
			
		||||
} from "./types";
 | 
			
		||||
 | 
			
		||||
export const shouldTestInside = (element: ExcalidrawElement) => {
 | 
			
		||||
  if (element.type === "arrow") {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const isDraggableFromInside =
 | 
			
		||||
    !isTransparent(element.backgroundColor) ||
 | 
			
		||||
    hasBoundTextElement(element) ||
 | 
			
		||||
    isIframeLikeElement(element) ||
 | 
			
		||||
    isTextElement(element);
 | 
			
		||||
 | 
			
		||||
  if (element.type === "line") {
 | 
			
		||||
    return isDraggableFromInside && isPathALoop(element.points);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (element.type === "freedraw") {
 | 
			
		||||
    return isDraggableFromInside && isPathALoop(element.points);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return isDraggableFromInside || isImageElement(element);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type HitTestArgs = {
 | 
			
		||||
  point: GlobalPoint;
 | 
			
		||||
  element: ExcalidrawElement;
 | 
			
		||||
  threshold: number;
 | 
			
		||||
  elementsMap: ElementsMap;
 | 
			
		||||
  frameNameBound?: FrameNameBounds | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const hitElementItself = ({
 | 
			
		||||
  point,
 | 
			
		||||
  element,
 | 
			
		||||
  threshold,
 | 
			
		||||
  elementsMap,
 | 
			
		||||
  frameNameBound = null,
 | 
			
		||||
}: HitTestArgs) => {
 | 
			
		||||
  // Hit test against a frame's name
 | 
			
		||||
  const hitFrameName = frameNameBound
 | 
			
		||||
    ? isPointWithinBounds(
 | 
			
		||||
        pointFrom(frameNameBound.x - threshold, frameNameBound.y - threshold),
 | 
			
		||||
        point,
 | 
			
		||||
        pointFrom(
 | 
			
		||||
          frameNameBound.x + frameNameBound.width + threshold,
 | 
			
		||||
          frameNameBound.y + frameNameBound.height + threshold,
 | 
			
		||||
        ),
 | 
			
		||||
      )
 | 
			
		||||
    : false;
 | 
			
		||||
 | 
			
		||||
  // Hit test against the extended, rotated bounding box of the element first
 | 
			
		||||
  const bounds = getElementBounds(element, elementsMap, true);
 | 
			
		||||
  const hitBounds = isPointWithinBounds(
 | 
			
		||||
    pointFrom(bounds[0] - threshold, bounds[1] - threshold),
 | 
			
		||||
    pointRotateRads(
 | 
			
		||||
      point,
 | 
			
		||||
      getCenterForBounds(bounds),
 | 
			
		||||
      -element.angle as Radians,
 | 
			
		||||
    ),
 | 
			
		||||
    pointFrom(bounds[2] + threshold, bounds[3] + threshold),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // PERF: Bail out early if the point is not even in the
 | 
			
		||||
  // rotated bounding box or not hitting the frame name (saves 99%)
 | 
			
		||||
  if (!hitBounds && !hitFrameName) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Do the precise (and relatively costly) hit test
 | 
			
		||||
  const hitElement = shouldTestInside(element)
 | 
			
		||||
    ? // Since `inShape` tests STRICTLY againt the insides of a shape
 | 
			
		||||
      // we would need `onShape` as well to include the "borders"
 | 
			
		||||
      isPointInElement(point, element, elementsMap) ||
 | 
			
		||||
      isPointOnElementOutline(point, element, elementsMap, threshold)
 | 
			
		||||
    : isPointOnElementOutline(point, element, elementsMap, threshold);
 | 
			
		||||
 | 
			
		||||
  return hitElement || hitFrameName;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const hitElementBoundingBox = (
 | 
			
		||||
  point: GlobalPoint,
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  tolerance = 0,
 | 
			
		||||
) => {
 | 
			
		||||
  let [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
 | 
			
		||||
  x1 -= tolerance;
 | 
			
		||||
  y1 -= tolerance;
 | 
			
		||||
  x2 += tolerance;
 | 
			
		||||
  y2 += tolerance;
 | 
			
		||||
  return isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const hitElementBoundingBoxOnly = (
 | 
			
		||||
  hitArgs: HitTestArgs,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
) =>
 | 
			
		||||
  !hitElementItself(hitArgs) &&
 | 
			
		||||
  // bound text is considered part of the element (even if it's outside the bounding box)
 | 
			
		||||
  !hitElementBoundText(hitArgs.point, hitArgs.element, elementsMap) &&
 | 
			
		||||
  hitElementBoundingBox(hitArgs.point, hitArgs.element, elementsMap);
 | 
			
		||||
 | 
			
		||||
export const hitElementBoundText = (
 | 
			
		||||
  point: GlobalPoint,
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
): boolean => {
 | 
			
		||||
  const boundTextElementCandidate = getBoundTextElement(element, elementsMap);
 | 
			
		||||
 | 
			
		||||
  if (!boundTextElementCandidate) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  const boundTextElement = isLinearElement(element)
 | 
			
		||||
    ? {
 | 
			
		||||
        ...boundTextElementCandidate,
 | 
			
		||||
        // arrow's bound text accurate position is not stored in the element's property
 | 
			
		||||
        // but rather calculated and returned from the following static method
 | 
			
		||||
        ...LinearElementEditor.getBoundTextElementPosition(
 | 
			
		||||
          element,
 | 
			
		||||
          boundTextElementCandidate,
 | 
			
		||||
          elementsMap,
 | 
			
		||||
        ),
 | 
			
		||||
      }
 | 
			
		||||
    : boundTextElementCandidate;
 | 
			
		||||
 | 
			
		||||
  return isPointInElement(point, boundTextElement, elementsMap);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Intersect a line with an element for binding test
 | 
			
		||||
 *
 | 
			
		||||
 * @param element
 | 
			
		||||
 * @param line
 | 
			
		||||
 * @param offset
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export const intersectElementWithLineSegment = (
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  line: LineSegment<GlobalPoint>,
 | 
			
		||||
  offset: number = 0,
 | 
			
		||||
  onlyFirst = false,
 | 
			
		||||
): GlobalPoint[] => {
 | 
			
		||||
  // First check if the line intersects the element's axis-aligned bounding box
 | 
			
		||||
  // as it is much faster than checking intersection against the element's shape
 | 
			
		||||
  const intersectorBounds = [
 | 
			
		||||
    Math.min(line[0][0] - offset, line[1][0] - offset),
 | 
			
		||||
    Math.min(line[0][1] - offset, line[1][1] - offset),
 | 
			
		||||
    Math.max(line[0][0] + offset, line[1][0] + offset),
 | 
			
		||||
    Math.max(line[0][1] + offset, line[1][1] + offset),
 | 
			
		||||
  ] as Bounds;
 | 
			
		||||
  const elementBounds = getElementBounds(element, elementsMap);
 | 
			
		||||
 | 
			
		||||
  if (!doBoundsIntersect(intersectorBounds, elementBounds)) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Do the actual intersection test against the element's shape
 | 
			
		||||
  switch (element.type) {
 | 
			
		||||
    case "rectangle":
 | 
			
		||||
    case "image":
 | 
			
		||||
    case "text":
 | 
			
		||||
    case "iframe":
 | 
			
		||||
    case "embeddable":
 | 
			
		||||
    case "frame":
 | 
			
		||||
    case "selection":
 | 
			
		||||
    case "magicframe":
 | 
			
		||||
      return intersectRectanguloidWithLineSegment(
 | 
			
		||||
        element,
 | 
			
		||||
        elementsMap,
 | 
			
		||||
        line,
 | 
			
		||||
        offset,
 | 
			
		||||
        onlyFirst,
 | 
			
		||||
      );
 | 
			
		||||
    case "diamond":
 | 
			
		||||
      return intersectDiamondWithLineSegment(
 | 
			
		||||
        element,
 | 
			
		||||
        elementsMap,
 | 
			
		||||
        line,
 | 
			
		||||
        offset,
 | 
			
		||||
        onlyFirst,
 | 
			
		||||
      );
 | 
			
		||||
    case "ellipse":
 | 
			
		||||
      return intersectEllipseWithLineSegment(
 | 
			
		||||
        element,
 | 
			
		||||
        elementsMap,
 | 
			
		||||
        line,
 | 
			
		||||
        offset,
 | 
			
		||||
      );
 | 
			
		||||
    case "line":
 | 
			
		||||
    case "freedraw":
 | 
			
		||||
    case "arrow":
 | 
			
		||||
      return intersectLinearOrFreeDrawWithLineSegment(element, line, onlyFirst);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const curveIntersections = (
 | 
			
		||||
  curves: Curve<GlobalPoint>[],
 | 
			
		||||
  segment: LineSegment<GlobalPoint>,
 | 
			
		||||
  intersections: GlobalPoint[],
 | 
			
		||||
  center: GlobalPoint,
 | 
			
		||||
  angle: Radians,
 | 
			
		||||
  onlyFirst = false,
 | 
			
		||||
) => {
 | 
			
		||||
  for (const c of curves) {
 | 
			
		||||
    // Optimize by doing a cheap bounding box check first
 | 
			
		||||
    const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]);
 | 
			
		||||
    const b2 = [
 | 
			
		||||
      Math.min(segment[0][0], segment[1][0]),
 | 
			
		||||
      Math.min(segment[0][1], segment[1][1]),
 | 
			
		||||
      Math.max(segment[0][0], segment[1][0]),
 | 
			
		||||
      Math.max(segment[0][1], segment[1][1]),
 | 
			
		||||
    ] as Bounds;
 | 
			
		||||
 | 
			
		||||
    if (!doBoundsIntersect(b1, b2)) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const hits = curveIntersectLineSegment(c, segment);
 | 
			
		||||
 | 
			
		||||
    if (hits.length > 0) {
 | 
			
		||||
      for (const j of hits) {
 | 
			
		||||
        intersections.push(pointRotateRads(j, center, angle));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (onlyFirst) {
 | 
			
		||||
        return intersections;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return intersections;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const lineIntersections = (
 | 
			
		||||
  lines: LineSegment<GlobalPoint>[],
 | 
			
		||||
  segment: LineSegment<GlobalPoint>,
 | 
			
		||||
  intersections: GlobalPoint[],
 | 
			
		||||
  center: GlobalPoint,
 | 
			
		||||
  angle: Radians,
 | 
			
		||||
  onlyFirst = false,
 | 
			
		||||
) => {
 | 
			
		||||
  for (const l of lines) {
 | 
			
		||||
    const intersection = lineSegmentIntersectionPoints(l, segment);
 | 
			
		||||
    if (intersection) {
 | 
			
		||||
      intersections.push(pointRotateRads(intersection, center, angle));
 | 
			
		||||
 | 
			
		||||
      if (onlyFirst) {
 | 
			
		||||
        return intersections;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return intersections;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const intersectLinearOrFreeDrawWithLineSegment = (
 | 
			
		||||
  element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
 | 
			
		||||
  segment: LineSegment<GlobalPoint>,
 | 
			
		||||
  onlyFirst = false,
 | 
			
		||||
): GlobalPoint[] => {
 | 
			
		||||
  // NOTE: This is the only one which return the decomposed elements
 | 
			
		||||
  // rotated! This is due to taking advantage of roughjs definitions.
 | 
			
		||||
  const [lines, curves] = deconstructLinearOrFreeDrawElement(element);
 | 
			
		||||
  const intersections: GlobalPoint[] = [];
 | 
			
		||||
 | 
			
		||||
  for (const l of lines) {
 | 
			
		||||
    const intersection = lineSegmentIntersectionPoints(l, segment);
 | 
			
		||||
    if (intersection) {
 | 
			
		||||
      intersections.push(intersection);
 | 
			
		||||
 | 
			
		||||
      if (onlyFirst) {
 | 
			
		||||
        return intersections;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const c of curves) {
 | 
			
		||||
    // Optimize by doing a cheap bounding box check first
 | 
			
		||||
    const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]);
 | 
			
		||||
    const b2 = [
 | 
			
		||||
      Math.min(segment[0][0], segment[1][0]),
 | 
			
		||||
      Math.min(segment[0][1], segment[1][1]),
 | 
			
		||||
      Math.max(segment[0][0], segment[1][0]),
 | 
			
		||||
      Math.max(segment[0][1], segment[1][1]),
 | 
			
		||||
    ] as Bounds;
 | 
			
		||||
 | 
			
		||||
    if (!doBoundsIntersect(b1, b2)) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const hits = curveIntersectLineSegment(c, segment);
 | 
			
		||||
 | 
			
		||||
    if (hits.length > 0) {
 | 
			
		||||
      intersections.push(...hits);
 | 
			
		||||
 | 
			
		||||
      if (onlyFirst) {
 | 
			
		||||
        return intersections;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return intersections;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const intersectRectanguloidWithLineSegment = (
 | 
			
		||||
  element: ExcalidrawRectanguloidElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  segment: LineSegment<GlobalPoint>,
 | 
			
		||||
  offset: number = 0,
 | 
			
		||||
  onlyFirst = false,
 | 
			
		||||
): GlobalPoint[] => {
 | 
			
		||||
  const center = elementCenterPoint(element, elementsMap);
 | 
			
		||||
  // To emulate a rotated rectangle we rotate the point in the inverse angle
 | 
			
		||||
  // instead. It's all the same distance-wise.
 | 
			
		||||
  const rotatedA = pointRotateRads<GlobalPoint>(
 | 
			
		||||
    segment[0],
 | 
			
		||||
    center,
 | 
			
		||||
    -element.angle as Radians,
 | 
			
		||||
  );
 | 
			
		||||
  const rotatedB = pointRotateRads<GlobalPoint>(
 | 
			
		||||
    segment[1],
 | 
			
		||||
    center,
 | 
			
		||||
    -element.angle as Radians,
 | 
			
		||||
  );
 | 
			
		||||
  const rotatedIntersector = lineSegment(rotatedA, rotatedB);
 | 
			
		||||
 | 
			
		||||
  // Get the element's building components we can test against
 | 
			
		||||
  const [sides, corners] = deconstructRectanguloidElement(element, offset);
 | 
			
		||||
 | 
			
		||||
  const intersections: GlobalPoint[] = [];
 | 
			
		||||
 | 
			
		||||
  lineIntersections(
 | 
			
		||||
    sides,
 | 
			
		||||
    rotatedIntersector,
 | 
			
		||||
    intersections,
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
    onlyFirst,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (onlyFirst && intersections.length > 0) {
 | 
			
		||||
    return intersections;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  curveIntersections(
 | 
			
		||||
    corners,
 | 
			
		||||
    rotatedIntersector,
 | 
			
		||||
    intersections,
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
    onlyFirst,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return intersections;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @param element
 | 
			
		||||
 * @param a
 | 
			
		||||
 * @param b
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
const intersectDiamondWithLineSegment = (
 | 
			
		||||
  element: ExcalidrawDiamondElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  l: LineSegment<GlobalPoint>,
 | 
			
		||||
  offset: number = 0,
 | 
			
		||||
  onlyFirst = false,
 | 
			
		||||
): GlobalPoint[] => {
 | 
			
		||||
  const center = elementCenterPoint(element, elementsMap);
 | 
			
		||||
 | 
			
		||||
  // Rotate the point to the inverse direction to simulate the rotated diamond
 | 
			
		||||
  // points. It's all the same distance-wise.
 | 
			
		||||
  const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
 | 
			
		||||
  const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
 | 
			
		||||
  const rotatedIntersector = lineSegment(rotatedA, rotatedB);
 | 
			
		||||
 | 
			
		||||
  const [sides, corners] = deconstructDiamondElement(element, offset);
 | 
			
		||||
  const intersections: GlobalPoint[] = [];
 | 
			
		||||
 | 
			
		||||
  lineIntersections(
 | 
			
		||||
    sides,
 | 
			
		||||
    rotatedIntersector,
 | 
			
		||||
    intersections,
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
    onlyFirst,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (onlyFirst && intersections.length > 0) {
 | 
			
		||||
    return intersections;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  curveIntersections(
 | 
			
		||||
    corners,
 | 
			
		||||
    rotatedIntersector,
 | 
			
		||||
    intersections,
 | 
			
		||||
    center,
 | 
			
		||||
    element.angle,
 | 
			
		||||
    onlyFirst,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return intersections;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @param element
 | 
			
		||||
 * @param a
 | 
			
		||||
 * @param b
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
const intersectEllipseWithLineSegment = (
 | 
			
		||||
  element: ExcalidrawEllipseElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  l: LineSegment<GlobalPoint>,
 | 
			
		||||
  offset: number = 0,
 | 
			
		||||
): GlobalPoint[] => {
 | 
			
		||||
  const center = elementCenterPoint(element, elementsMap);
 | 
			
		||||
 | 
			
		||||
  const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
 | 
			
		||||
  const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
 | 
			
		||||
 | 
			
		||||
  return ellipseSegmentInterceptPoints(
 | 
			
		||||
    ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
 | 
			
		||||
    lineSegment(rotatedA, rotatedB),
 | 
			
		||||
  ).map((p) => pointRotateRads(p, center, element.angle));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if the given point is considered on the given shape's border
 | 
			
		||||
 *
 | 
			
		||||
 * @param point
 | 
			
		||||
 * @param element
 | 
			
		||||
 * @param tolerance
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
const isPointOnElementOutline = (
 | 
			
		||||
  point: GlobalPoint,
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
  tolerance = 1,
 | 
			
		||||
) => distanceToElement(element, elementsMap, point) <= tolerance;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if the given point is considered inside the element's border
 | 
			
		||||
 *
 | 
			
		||||
 * @param point
 | 
			
		||||
 * @param element
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export const isPointInElement = (
 | 
			
		||||
  point: GlobalPoint,
 | 
			
		||||
  element: ExcalidrawElement,
 | 
			
		||||
  elementsMap: ElementsMap,
 | 
			
		||||
) => {
 | 
			
		||||
  if (
 | 
			
		||||
    (isLinearElement(element) || isFreeDrawElement(element)) &&
 | 
			
		||||
    !isPathALoop(element.points)
 | 
			
		||||
  ) {
 | 
			
		||||
    // There isn't any "inside" for a non-looping path
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
 | 
			
		||||
 | 
			
		||||
  if (!isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2))) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
 | 
			
		||||
  const otherPoint = pointFromVector(
 | 
			
		||||
    vectorScale(
 | 
			
		||||
      vectorNormalize(vectorFromPoint(point, center, 0.1)),
 | 
			
		||||
      Math.max(element.width, element.height) * 2,
 | 
			
		||||
    ),
 | 
			
		||||
    center,
 | 
			
		||||
  );
 | 
			
		||||
  const intersector = lineSegment(point, otherPoint);
 | 
			
		||||
  const intersections = intersectElementWithLineSegment(
 | 
			
		||||
    element,
 | 
			
		||||
    elementsMap,
 | 
			
		||||
    intersector,
 | 
			
		||||
  ).filter((p, pos, arr) => arr.findIndex((q) => pointsEqual(q, p)) === pos);
 | 
			
		||||
 | 
			
		||||
  return intersections.length % 2 === 1;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import type { ElementOrToolType } from "../types";
 | 
			
		||||
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
 | 
			
		||||
 | 
			
		||||
export const hasBackground = (type: ElementOrToolType) =>
 | 
			
		||||
  type === "rectangle" ||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user